Compare commits

...

13 Commits

Author SHA1 Message Date
Prowler Bot
dbc8735c52 perf(api): deduplicate nodes before ProwlerFinding lookup in Attack Paths queries (#10432)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-03-23 17:56:46 +01:00
Prowler Bot
b06379badc fix(api): Update Flask and Werkzeug to address vulnerabilities (#10431)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-03-23 17:38:15 +01:00
Prowler Bot
a002ec8190 fix(sdk): ignore disabled users in Entra MFA check (#10429)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-03-23 15:30:01 +00:00
Prowler Bot
d7d0f197a9 fix(sdk): use case-insensitive comparison for Azure MySQL flexible server checks (#10418)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-03-23 10:07:07 +00:00
Prowler Bot
21e1f583f7 fix(sdk): use case-insensitive comparison for Azure VM backup checks (#10417)
Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com>
2026-03-23 09:53:52 +00:00
Prowler Bot
4d5c38ca76 chore(api): Bump version to v1.22.2 (#10406)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-20 09:59:11 +01:00
Prowler Bot
9cc5513c5e chore(release): Bump version to v5.21.2 (#10405)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-20 09:58:48 +01:00
Prowler Bot
0bc3f69032 docs: Update version to v5.21.1 (#10407)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-20 09:56:27 +01:00
Pepe Fagoaga
73dde783da chore(release): bump API for v1.22.1 (#10404) 2026-03-20 08:18:43 +01:00
Prowler Bot
9ae35029dc perf(api): replace JOINs with pre-check in threat score aggregation query (#10399)
Co-authored-by: Josema Camacho <josema@prowler.com>
2026-03-19 17:43:47 +01:00
Prowler Bot
cd9d7a2e95 docs: Update version to v5.21.0 (#10392)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:41:06 +01:00
Prowler Bot
ab9c5b0f35 chore(release): Bump version to v5.21.1 (#10390)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 13:40:39 +01:00
Prowler Bot
1b3ed72f0d chore(api): Update prowler dependency to v5.21 for release 5.21.0 (#10385)
Co-authored-by: prowler-bot <179230569+prowler-bot@users.noreply.github.com>
2026-03-19 11:30:42 +01:00
26 changed files with 1515 additions and 305 deletions

View File

@@ -2,6 +2,39 @@
All notable changes to the **Prowler API** are documented in this file.
## [1.23.0] (Prowler UNRELEASED)
### 🐞 Fixed
- Finding groups latest endpoint now aggregates the latest snapshot per provider before check-level totals, keeping impacted resources aligned across providers [(#10419)](https://github.com/prowler-cloud/prowler/pull/10419)
- Mute rule creation now triggers finding-group summary re-aggregation after historical muting, keeping stats in sync after mute operations [(#10419)](https://github.com/prowler-cloud/prowler/pull/10419)
### 🔐 Security
- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165)
---
## [1.22.2] (Prowler UNRELEASED)
### 🐞 Fixed
- Attack Paths: Deduplicate nodes before ProwlerFinding lookup in Attack Paths Cypher queries, reducing execution time [(#10424)](https://github.com/prowler-cloud/prowler/pull/10424)
### 🔐 Security
- Bump `flask` to 3.1.3 (CVE-2026-27205) and `werkzeug` to 3.1.6 (CVE-2026-27199) [(#10430)](https://github.com/prowler-cloud/prowler/pull/10430)
---
## [1.22.1] (Prowler v5.21.1)
### 🐞 Fixed
- ThreatScore aggregation query to eliminate unnecessary JOINs and `COUNT(DISTINCT)` overhead [(#10394)](https://github.com/prowler-cloud/prowler/pull/10394)
---
## [1.22.0] (Prowler v5.21.0)
### 🚀 Added

424
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ dependencies = [
"drf-spectacular-jsonapi==0.5.1",
"gunicorn==23.0.0",
"lxml==5.3.2",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@master",
"prowler @ git+https://github.com/prowler-cloud/prowler.git@v5.21",
"psycopg2-binary==2.9.9",
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
@@ -49,7 +49,7 @@ name = "prowler-api"
package-mode = false
# Needed for the SDK compatibility
requires-python = ">=3.11,<3.13"
version = "1.22.0"
version = "1.22.2"
[project.scripts]
celery = "src.backend.config.settings.celery"

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: Prowler API
version: 1.22.0
version: 1.22.2
description: |-
Prowler API specification.

View File

@@ -408,7 +408,7 @@ class SchemaView(SpectacularAPIView):
def get(self, request, *args, **kwargs):
spectacular_settings.TITLE = "Prowler API"
spectacular_settings.VERSION = "1.22.0"
spectacular_settings.VERSION = "1.22.2"
spectacular_settings.DESCRIPTION = (
"Prowler API specification.\n\nThis file is auto-generated."
)

View File

@@ -4,7 +4,7 @@ from django.db.models import Count, Q
from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding, StatusChoices
from api.models import Finding, Scan, StatusChoices
from prowler.lib.outputs.finding import Finding as FindingOutput
logger = get_task_logger(__name__)
@@ -35,25 +35,26 @@ def _aggregate_requirement_statistics_from_database(
}
"""
requirement_statistics_by_check_id = {}
# TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes
# TODO: review when finding-resource relation changes from 1:1
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Pre-check: skip if the scan's provider is deleted (avoids JOINs in the main query)
if Scan.all_objects.filter(id=scan_id, provider__is_deleted=True).exists():
return requirement_statistics_by_check_id
aggregated_statistics_queryset = (
Finding.all_objects.filter(
tenant_id=tenant_id,
scan_id=scan_id,
muted=False,
resources__provider__is_deleted=False,
)
.values("check_id")
.annotate(
total_findings=Count(
"id",
distinct=True,
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
),
passed_findings=Count(
"id",
distinct=True,
filter=Q(status=StatusChoices.PASS),
),
)

View File

@@ -169,35 +169,27 @@ class TestAggregateRequirementStatistics:
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture):
"""Verify findings without resources are excluded from aggregation."""
def test_skips_aggregation_for_deleted_provider(
self, tenants_fixture, scans_fixture
):
"""Verify aggregation returns empty when the scan's provider is soft-deleted."""
tenant = tenants_fixture[0]
scan = scans_fixture[0]
# Finding WITH resource → should be counted
self._create_finding_with_resource(
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
)
# Finding WITHOUT resource → should be EXCLUDED
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
# Soft-delete the provider
provider = scan.provider
provider.is_deleted = True
provider.save(update_fields=["is_deleted"])
result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
assert result == {}
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
"""Verify a finding with multiple resources is only counted once."""

View File

@@ -121,8 +121,8 @@ To update the environment file:
Edit the `.env` file and change version values:
```env
PROWLER_UI_VERSION="5.20.0"
PROWLER_API_VERSION="5.20.0"
PROWLER_UI_VERSION="5.21.1"
PROWLER_API_VERSION="5.21.1"
```
<Note>

View File

@@ -2,6 +2,16 @@
All notable changes to the **Prowler SDK** are documented in this file.
## [5.21.2] (Prowler UNRELEASED)
### 🐞 Fixed
- Azure MySQL flexible server checks now compare configuration values case-insensitively to avoid false negatives when Azure returns lowercase values [(#10396)](https://github.com/prowler-cloud/prowler/pull/10396)
- Azure `vm_backup_enabled` and `vm_sufficient_daily_backup_retention_period` checks now compare VM names case-insensitively to avoid false negatives when Azure stores backup item names in a different case [(#10395)](https://github.com/prowler-cloud/prowler/pull/10395)
- `entra_non_privileged_user_has_mfa` skips disabled users to avoid false positives [(#10426)](https://github.com/prowler-cloud/prowler/pull/10426)
---
## [5.21.0] (Prowler v5.21.0)
### 🚀 Added

View File

@@ -38,7 +38,7 @@ class _MutableTimestamp:
timestamp = _MutableTimestamp(datetime.today())
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
prowler_version = "5.21.0"
prowler_version = "5.21.2"
html_logo_url = "https://github.com/prowler-cloud/prowler/"
square_logo_img = "https://raw.githubusercontent.com/prowler-cloud/prowler/dc7d2d5aeb92fdf12e8604f42ef6472cd3e8e889/docs/img/prowler-logo-black.png"
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"

View File

@@ -11,7 +11,7 @@ class entra_non_privileged_user_has_mfa(Check):
for tenant_domain, users in entra_client.users.items():
for user in users.values():
if not is_privileged_user(
if user.account_enabled and not is_privileged_user(
user, entra_client.directory_roles[tenant_domain]
):
report = Check_Report_Azure(metadata=self.metadata(), resource=user)

View File

@@ -3,7 +3,9 @@ from asyncio import gather
from typing import List, Optional
from uuid import UUID
from kiota_abstractions.base_request_configuration import RequestConfiguration
from msgraph import GraphServiceClient
from msgraph.generated.users.users_request_builder import UsersRequestBuilder
from pydantic.v1 import BaseModel
from prowler.lib.logger import logger
@@ -65,9 +67,16 @@ class Entra(AzureService):
logger.info("Entra - Getting users...")
users = {}
try:
request_configuration = RequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=["id", "displayName", "accountEnabled"]
)
)
for tenant, client in self.clients.items():
users.update({tenant: {}})
users_response = await client.users.get()
users_response = await client.users.get(
request_configuration=request_configuration
)
registration_details = await self._get_user_registration_details(client)
try:
@@ -81,6 +90,9 @@ class Entra(AzureService):
is_mfa_capable=registration_details.get(
user.id, False
),
account_enabled=getattr(
user, "account_enabled", True
),
)
}
)
@@ -409,6 +421,7 @@ class User(BaseModel):
id: str
name: str
is_mfa_capable: bool = False
account_enabled: bool = True
class DefaultUserRolePermissions(BaseModel):

View File

@@ -21,9 +21,9 @@ class mysql_flexible_server_audit_log_connection_activated(Check):
"audit_log_events"
].resource_id
if "CONNECTION" in server.configurations[
if "connection" in server.configurations[
"audit_log_events"
].value.split(","):
].value.lower().split(","):
report.status = "PASS"
report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name}."

View File

@@ -21,7 +21,7 @@ class mysql_flexible_server_audit_log_enabled(Check):
"audit_log_enabled"
].resource_id
if server.configurations["audit_log_enabled"].value == "ON":
if server.configurations["audit_log_enabled"].value.lower() == "on":
report.status = "PASS"
report.status_extended = f"Audit log is enabled for server {server.name} in subscription {subscription_name}."

View File

@@ -20,7 +20,10 @@ class mysql_flexible_server_ssl_connection_enabled(Check):
report.resource_id = server.configurations[
"require_secure_transport"
].resource_id
if server.configurations["require_secure_transport"].value == "ON":
if (
server.configurations["require_secure_transport"].value.lower()
== "on"
):
report.status = "PASS"
report.status_extended = f"SSL connection is enabled for server {server.name} in subscription {subscription_name}."

View File

@@ -31,7 +31,8 @@ class vm_backup_enabled(Check):
for backup_item in vault.backup_protected_items.values():
if (
backup_item.workload_type == DataSourceType.VM
and backup_item.name.split(";")[-1] == vm.resource_name
and backup_item.name.split(";")[-1].lower()
== vm.resource_name.lower()
):
found = True
found_vault_name = vault.name

View File

@@ -27,7 +27,8 @@ class vm_sufficient_daily_backup_retention_period(Check):
for backup_item in vault.backup_protected_items.values():
if (
backup_item.workload_type == DataSourceType.VM
and backup_item.name.split(";")[-1] == vm.resource_name
and backup_item.name.split(";")[-1].lower()
== vm.resource_name.lower()
):
backup_found = True
policy_id = backup_item.backup_policy_id

View File

@@ -94,7 +94,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
name = "prowler"
readme = "README.md"
requires-python = ">3.9.1,<3.13"
version = "5.21.0"
version = "5.21.2"
[project.scripts]
prowler = "prowler.__main__:prowler"

View File

@@ -142,6 +142,86 @@ class Test_entra_non_privileged_user_has_mfa:
assert result[0].resource_id == user_id
assert result[0].subscription == f"Tenant: {DOMAIN}"
def test_entra_disabled_user_no_privileged_no_mfa(self):
entra_client = mock.MagicMock
user_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import (
entra_non_privileged_user_has_mfa,
)
from prowler.providers.azure.services.entra.entra_service import (
DirectoryRole,
User,
)
user = User(
id=user_id,
name="foo",
is_mfa_capable=False,
account_enabled=False,
)
entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}}
entra_client.directory_roles = {
DOMAIN: {
"Global Administrator": DirectoryRole(id=str(uuid4()), members=[])
}
}
check = entra_non_privileged_user_has_mfa()
result = check.execute()
assert len(result) == 0
def test_entra_disabled_user_no_privileged_mfa(self):
entra_client = mock.MagicMock
user_id = str(uuid4())
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client",
new=entra_client,
),
):
from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import (
entra_non_privileged_user_has_mfa,
)
from prowler.providers.azure.services.entra.entra_service import (
DirectoryRole,
User,
)
user = User(
id=user_id,
name="foo",
is_mfa_capable=True,
account_enabled=False,
)
entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}}
entra_client.directory_roles = {
DOMAIN: {
"Global Administrator": DirectoryRole(id=str(uuid4()), members=[])
}
}
check = entra_non_privileged_user_has_mfa()
result = check.execute()
assert len(result) == 0
def test_entra_user_privileged_no_mfa(self):
entra_client = mock.MagicMock
user_id = str(uuid4())

View File

@@ -147,6 +147,7 @@ class Test_Entra_Service:
assert entra_client.users[DOMAIN]["user-1@tenant1.es"].id == "id-1"
assert entra_client.users[DOMAIN]["user-1@tenant1.es"].name == "User 1"
assert entra_client.users[DOMAIN]["user-1@tenant1.es"].is_mfa_capable is False
assert entra_client.users[DOMAIN]["user-1@tenant1.es"].account_enabled is True
def test_get_authorization_policy(self):
entra_client = Entra(set_mocked_azure_provider())
@@ -229,8 +230,8 @@ def test_azure_entra__get_users_handles_pagination():
entra_service = Entra.__new__(Entra)
users_page_one = [
SimpleNamespace(id="user-1", display_name="User 1"),
SimpleNamespace(id="user-2", display_name="User 2"),
SimpleNamespace(id="user-1", display_name="User 1", account_enabled=False),
SimpleNamespace(id="user-2", display_name="User 2", account_enabled=True),
]
users_page_two = [
SimpleNamespace(id="user-3", display_name="User 3"),
@@ -288,9 +289,18 @@ def test_azure_entra__get_users_handles_pagination():
assert len(users["tenant-1"]) == 3
assert users_builder.get.await_count == 1
request_configuration = users_builder.get.await_args.kwargs["request_configuration"]
assert request_configuration.query_parameters.select == [
"id",
"displayName",
"accountEnabled",
]
with_url_mock.assert_called_once_with("next-link")
registration_details_builder.get.assert_awaited()
registration_details_builder.with_url.assert_not_called()
assert users["tenant-1"]["user-1"].is_mfa_capable is True
assert users["tenant-1"]["user-1"].account_enabled is False
assert users["tenant-1"]["user-2"].is_mfa_capable is True
assert users["tenant-1"]["user-2"].account_enabled is True
assert users["tenant-1"]["user-3"].is_mfa_capable is False
assert users["tenant-1"]["user-3"].account_enabled is True

View File

@@ -56,6 +56,57 @@ class Test_mysql_flexible_server_audit_log_connection_activated:
result = check.execute()
assert len(result) == 0
def test_mysql_audit_log_connection_activated_lowercase(self):
server_name = str(uuid4())
mysql_client = mock.MagicMock
mysql_client.flexible_servers = {
AZURE_SUBSCRIPTION_ID: {
"/subscriptions/resource_id": FlexibleServer(
resource_id="/subscriptions/resource_id",
name=server_name,
location="location",
version="version",
configurations={
"audit_log_events": Configuration(
resource_id=f"/subscriptions/{server_name}/configurations/audit_log_events",
description="description",
value="connection",
)
},
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_connection_activated.mysql_flexible_server_audit_log_connection_activated.mysql_client",
new=mysql_client,
),
):
from prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_connection_activated.mysql_flexible_server_audit_log_connection_activated import (
mysql_flexible_server_audit_log_connection_activated,
)
check = mysql_flexible_server_audit_log_connection_activated()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == server_name
assert result[0].location == "location"
assert (
result[0].resource_id
== f"/subscriptions/{server_name}/configurations/audit_log_events"
)
assert (
result[0].status_extended
== f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}."
)
def test_mysql_audit_log_connection_not_connection(self):
server_name = str(uuid4())
mysql_client = mock.MagicMock

View File

@@ -56,6 +56,57 @@ class Test_mysql_flexible_server_audit_log_enabled:
result = check.execute()
assert len(result) == 0
def test_mysql_audit_log_enabled_lowercase(self):
server_name = str(uuid4())
mysql_client = mock.MagicMock
mysql_client.flexible_servers = {
AZURE_SUBSCRIPTION_ID: {
"/subscriptions/resource_id": FlexibleServer(
resource_id="/subscriptions/resource_id",
name=server_name,
location="location",
version="version",
configurations={
"audit_log_enabled": Configuration(
resource_id=f"/subscriptions/{server_name}/configurations/audit_log_enabled",
description="description",
value="on",
)
},
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_enabled.mysql_flexible_server_audit_log_enabled.mysql_client",
new=mysql_client,
),
):
from prowler.providers.azure.services.mysql.mysql_flexible_server_audit_log_enabled.mysql_flexible_server_audit_log_enabled import (
mysql_flexible_server_audit_log_enabled,
)
check = mysql_flexible_server_audit_log_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == server_name
assert result[0].location == "location"
assert (
result[0].resource_id
== f"/subscriptions/{server_name}/configurations/audit_log_enabled"
)
assert (
result[0].status_extended
== f"Audit log is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}."
)
def test_mysql_audit_log_disabled(self):
server_name = str(uuid4())
mysql_client = mock.MagicMock

View File

@@ -107,6 +107,57 @@ class Test_mysql_flexible_server_ssl_connection_enabled:
== f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}."
)
def test_mysql_connection_enabled_lowercase(self):
server_name = str(uuid4())
mysql_client = mock.MagicMock
mysql_client.flexible_servers = {
AZURE_SUBSCRIPTION_ID: {
"/subscriptions/resource_id": FlexibleServer(
resource_id="/subscriptions/resource_id",
name=server_name,
location="location",
version="version",
configurations={
"require_secure_transport": Configuration(
resource_id=f"/subscriptions/{server_name}/configurations/require_secure_transport",
description="description",
value="on",
)
},
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.mysql.mysql_flexible_server_ssl_connection_enabled.mysql_flexible_server_ssl_connection_enabled.mysql_client",
new=mysql_client,
),
):
from prowler.providers.azure.services.mysql.mysql_flexible_server_ssl_connection_enabled.mysql_flexible_server_ssl_connection_enabled import (
mysql_flexible_server_ssl_connection_enabled,
)
check = mysql_flexible_server_ssl_connection_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == server_name
assert result[0].location == "location"
assert (
result[0].resource_id
== f"/subscriptions/{server_name}/configurations/require_secure_transport"
)
assert (
result[0].status_extended
== f"SSL connection is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_ID}."
)
def test_mysql_ssl_connection_disabled(self):
server_name = str(uuid4())
mysql_client = mock.MagicMock

View File

@@ -221,6 +221,85 @@ class Test_vm_backup_enabled:
== f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is not protected by Azure Backup."
)
def test_vm_protected_by_backup_case_insensitive(self):
vm_id = str(uuid4())
vm_name = "vmtest"
vault_id = str(uuid4())
vault_name = "vault1"
mock_vm_client = mock.MagicMock()
mock_recovery_client = mock.MagicMock()
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client",
new=mock_vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client",
new=mock_recovery_client,
),
):
from azure.mgmt.recoveryservicesbackup.activestamp.models import (
DataSourceType,
)
from prowler.providers.azure.services.recovery.recovery_service import (
BackupItem,
BackupVault,
)
from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import (
vm_backup_enabled,
)
from prowler.providers.azure.services.vm.vm_service import (
ManagedDiskParameters,
OSDisk,
StorageProfile,
VirtualMachine,
)
vm = VirtualMachine(
resource_id=vm_id,
resource_name=vm_name,
location="eastus",
security_profile=None,
extensions=[],
storage_profile=StorageProfile(
os_disk=OSDisk(
name="os_disk_name",
operating_system_type="Linux",
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
),
data_disks=[],
),
)
backup_item = BackupItem(
id=str(uuid4()),
name="someprefix;VMTEST",
workload_type=DataSourceType.VM,
)
vault = BackupVault(
id=vault_id,
name=vault_name,
location="eastus",
backup_protected_items={backup_item.id: backup_item},
)
mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
check = vm_backup_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == vm_name
assert result[0].resource_id == vm_id
assert (
result[0].status_extended
== f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is protected by Azure Backup (vault: {vault_name})."
)
def test_vm_protected_by_backup_non_vm_workload(self):
vm_id = str(uuid4())
vm_name = "VMTest"

View File

@@ -156,6 +156,100 @@ class Test_vm_sufficient_daily_backup_retention_period:
in result[0].status_extended
)
def test_vm_with_sufficient_retention_case_insensitive(self):
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
from prowler.providers.azure.services.recovery.recovery_service import (
BackupItem,
BackupPolicy,
BackupVault,
)
from prowler.providers.azure.services.vm.vm_service import (
ManagedDiskParameters,
OSDisk,
StorageProfile,
VirtualMachine,
)
vm_id = str(uuid4())
vm_name = "vmtest"
vault_id = str(uuid4())
policy_id = str(uuid4())
retention_days = 14
min_retention_days = 7
vm = VirtualMachine(
resource_id=vm_id,
resource_name=vm_name,
location="eastus",
security_profile=None,
extensions=[],
storage_profile=StorageProfile(
os_disk=OSDisk(
name="os_disk_name",
operating_system_type="Linux",
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
),
data_disks=[],
),
)
backup_item = BackupItem(
id=str(uuid4()),
name="someprefix;VMTEST",
workload_type=DataSourceType.VM,
backup_policy_id=policy_id,
)
backup_policy = BackupPolicy(
id=policy_id,
name="policy1",
retention_days=retention_days,
)
vault = BackupVault(
id=vault_id,
name="vault1",
location="eastus",
backup_protected_items={backup_item.id: backup_item},
backup_policies={policy_id: backup_policy},
)
vm_client = mock.MagicMock()
recovery_client = mock.MagicMock()
vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
vm_client.audit_config = {
"vm_backup_min_daily_retention_days": min_retention_days
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(
audit_config=vm_client.audit_config
),
),
mock.patch(
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client",
new=vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client",
new=recovery_client,
),
):
from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import (
vm_sufficient_daily_backup_retention_period,
)
check = vm_sufficient_daily_backup_retention_period()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == vm_name
assert result[0].resource_id == vm_id
assert (
f"has a daily backup retention period of {retention_days} days"
in result[0].status_extended
)
def test_vm_with_insufficient_retention(self):
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType