mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-31 21:27:28 +00:00
Compare commits
13 Commits
fix/ui-fin
...
v5.21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbc8735c52 | ||
|
|
b06379badc | ||
|
|
a002ec8190 | ||
|
|
d7d0f197a9 | ||
|
|
21e1f583f7 | ||
|
|
4d5c38ca76 | ||
|
|
9cc5513c5e | ||
|
|
0bc3f69032 | ||
|
|
73dde783da | ||
|
|
9ae35029dc | ||
|
|
cd9d7a2e95 | ||
|
|
ab9c5b0f35 | ||
|
|
1b3ed72f0d |
@@ -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
424
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Prowler API
|
||||
version: 1.22.0
|
||||
version: 1.22.2
|
||||
description: |-
|
||||
Prowler API specification.
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}."
|
||||
|
||||
|
||||
@@ -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}."
|
||||
|
||||
|
||||
@@ -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}."
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user