mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-31 13:17:22 +00:00
Compare commits
5 Commits
fix/legacy
...
5.21.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73dde783da | ||
|
|
9ae35029dc | ||
|
|
cd9d7a2e95 | ||
|
|
ab9c5b0f35 | ||
|
|
1b3ed72f0d |
@@ -99,12 +99,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Pin prowler SDK to latest master commit
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
run: |
|
|
||||||
LATEST_SHA=$(git ls-remote https://github.com/prowler-cloud/prowler.git refs/heads/master | cut -f1)
|
|
||||||
sed -i "s|prowler-cloud/prowler.git@master|prowler-cloud/prowler.git@${LATEST_SHA}|" api/pyproject.toml
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to the **Prowler API** are documented in this file.
|
All notable changes to the **Prowler API** are documented in this file.
|
||||||
|
|
||||||
|
## [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)
|
## [1.22.0] (Prowler v5.21.0)
|
||||||
|
|
||||||
### 🚀 Added
|
### 🚀 Added
|
||||||
|
|||||||
414
api/poetry.lock
generated
414
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",
|
"drf-spectacular-jsonapi==0.5.1",
|
||||||
"gunicorn==23.0.0",
|
"gunicorn==23.0.0",
|
||||||
"lxml==5.3.2",
|
"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",
|
"psycopg2-binary==2.9.9",
|
||||||
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
|
"pytest-celery[redis] (>=1.0.1,<2.0.0)",
|
||||||
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
"sentry-sdk[django] (>=2.20.0,<3.0.0)",
|
||||||
@@ -49,7 +49,7 @@ name = "prowler-api"
|
|||||||
package-mode = false
|
package-mode = false
|
||||||
# Needed for the SDK compatibility
|
# Needed for the SDK compatibility
|
||||||
requires-python = ">=3.11,<3.13"
|
requires-python = ">=3.11,<3.13"
|
||||||
version = "1.23.0"
|
version = "1.22.1"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
celery = "src.backend.config.settings.celery"
|
celery = "src.backend.config.settings.celery"
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# Generated by Django 5.1.15 on 2026-03-18
|
|
||||||
|
|
||||||
from django.contrib.postgres.indexes import GinIndex, OpClass
|
|
||||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
|
||||||
from django.db import migrations
|
|
||||||
from django.db.models.functions import Upper
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
atomic = False
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("api", "0084_googleworkspace_provider"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
AddIndexConcurrently(
|
|
||||||
model_name="findinggroupdailysummary",
|
|
||||||
index=GinIndex(
|
|
||||||
OpClass(Upper("check_id"), name="gin_trgm_ops"),
|
|
||||||
name="fgds_check_id_trgm_idx",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
AddIndexConcurrently(
|
|
||||||
model_name="findinggroupdailysummary",
|
|
||||||
index=GinIndex(
|
|
||||||
OpClass(Upper("check_title"), name="gin_trgm_ops"),
|
|
||||||
name="fgds_check_title_trgm_idx",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1783,15 +1783,6 @@ class FindingGroupDailySummary(RowLevelSecurityProtectedModel):
|
|||||||
fields=["tenant_id", "provider", "inserted_at"],
|
fields=["tenant_id", "provider", "inserted_at"],
|
||||||
name="fgds_tenant_prov_ins_idx",
|
name="fgds_tenant_prov_ins_idx",
|
||||||
),
|
),
|
||||||
# Trigram indexes for case-insensitive search
|
|
||||||
GinIndex(
|
|
||||||
OpClass(Upper("check_id"), name="gin_trgm_ops"),
|
|
||||||
name="fgds_check_id_trgm_idx",
|
|
||||||
),
|
|
||||||
GinIndex(
|
|
||||||
OpClass(Upper("check_title"), name="gin_trgm_ops"),
|
|
||||||
name="fgds_check_title_trgm_idx",
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
class JSONAPIMeta:
|
class JSONAPIMeta:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: Prowler API
|
title: Prowler API
|
||||||
version: 1.23.0
|
version: 1.22.1
|
||||||
description: |-
|
description: |-
|
||||||
Prowler API specification.
|
Prowler API specification.
|
||||||
|
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ class SchemaView(SpectacularAPIView):
|
|||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
spectacular_settings.TITLE = "Prowler API"
|
spectacular_settings.TITLE = "Prowler API"
|
||||||
spectacular_settings.VERSION = "1.23.0"
|
spectacular_settings.VERSION = "1.22.1"
|
||||||
spectacular_settings.DESCRIPTION = (
|
spectacular_settings.DESCRIPTION = (
|
||||||
"Prowler API specification.\n\nThis file is auto-generated."
|
"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_router import READ_REPLICA_ALIAS
|
||||||
from api.db_utils import rls_transaction
|
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
|
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
@@ -35,25 +35,26 @@ def _aggregate_requirement_statistics_from_database(
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
requirement_statistics_by_check_id = {}
|
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):
|
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 = (
|
aggregated_statistics_queryset = (
|
||||||
Finding.all_objects.filter(
|
Finding.all_objects.filter(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
scan_id=scan_id,
|
scan_id=scan_id,
|
||||||
muted=False,
|
muted=False,
|
||||||
resources__provider__is_deleted=False,
|
|
||||||
)
|
)
|
||||||
.values("check_id")
|
.values("check_id")
|
||||||
.annotate(
|
.annotate(
|
||||||
total_findings=Count(
|
total_findings=Count(
|
||||||
"id",
|
"id",
|
||||||
distinct=True,
|
|
||||||
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
|
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
|
||||||
),
|
),
|
||||||
passed_findings=Count(
|
passed_findings=Count(
|
||||||
"id",
|
"id",
|
||||||
distinct=True,
|
|
||||||
filter=Q(status=StatusChoices.PASS),
|
filter=Q(status=StatusChoices.PASS),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -169,35 +169,27 @@ class TestAggregateRequirementStatistics:
|
|||||||
assert result["check_1"]["passed"] == 1
|
assert result["check_1"]["passed"] == 1
|
||||||
assert result["check_1"]["total"] == 1
|
assert result["check_1"]["total"] == 1
|
||||||
|
|
||||||
def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture):
|
def test_skips_aggregation_for_deleted_provider(
|
||||||
"""Verify findings without resources are excluded from aggregation."""
|
self, tenants_fixture, scans_fixture
|
||||||
|
):
|
||||||
|
"""Verify aggregation returns empty when the scan's provider is soft-deleted."""
|
||||||
tenant = tenants_fixture[0]
|
tenant = tenants_fixture[0]
|
||||||
scan = scans_fixture[0]
|
scan = scans_fixture[0]
|
||||||
|
|
||||||
# Finding WITH resource → should be counted
|
|
||||||
self._create_finding_with_resource(
|
self._create_finding_with_resource(
|
||||||
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
|
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
|
||||||
)
|
)
|
||||||
|
|
||||||
# Finding WITHOUT resource → should be EXCLUDED
|
# Soft-delete the provider
|
||||||
Finding.objects.create(
|
provider = scan.provider
|
||||||
tenant_id=tenant.id,
|
provider.is_deleted = True
|
||||||
scan=scan,
|
provider.save(update_fields=["is_deleted"])
|
||||||
uid="finding-2",
|
|
||||||
check_id="check_1",
|
|
||||||
status=StatusChoices.FAIL,
|
|
||||||
severity=Severity.high,
|
|
||||||
impact=Severity.high,
|
|
||||||
check_metadata={},
|
|
||||||
raw_result={},
|
|
||||||
)
|
|
||||||
|
|
||||||
result = _aggregate_requirement_statistics_from_database(
|
result = _aggregate_requirement_statistics_from_database(
|
||||||
str(tenant.id), str(scan.id)
|
str(tenant.id), str(scan.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["check_1"]["passed"] == 1
|
assert result == {}
|
||||||
assert result["check_1"]["total"] == 1
|
|
||||||
|
|
||||||
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
|
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
|
||||||
"""Verify a finding with multiple resources is only counted once."""
|
"""Verify a finding with multiple resources is only counted once."""
|
||||||
|
|||||||
@@ -334,172 +334,6 @@ class TestGenerateOutputs:
|
|||||||
output_location="s3://bucket/zipped.zip"
|
output_location="s3://bucket/zipped.zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("tasks.tasks._upload_to_s3")
|
|
||||||
@patch("tasks.tasks._compress_output_files")
|
|
||||||
@patch("tasks.tasks.get_compliance_frameworks")
|
|
||||||
@patch("tasks.tasks.Compliance.get_bulk")
|
|
||||||
@patch("tasks.tasks.initialize_prowler_provider")
|
|
||||||
@patch("tasks.tasks.Provider.objects.get")
|
|
||||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
|
||||||
@patch("tasks.tasks.Finding.all_objects.filter")
|
|
||||||
def test_generate_outputs_accepts_legacy_persisted_check_metadata(
|
|
||||||
self,
|
|
||||||
mock_finding_filter,
|
|
||||||
mock_scan_summary_filter,
|
|
||||||
mock_provider_get,
|
|
||||||
mock_initialize_provider,
|
|
||||||
mock_compliance_get_bulk,
|
|
||||||
mock_get_available_frameworks,
|
|
||||||
mock_compress,
|
|
||||||
mock_upload,
|
|
||||||
):
|
|
||||||
mock_scan_summary_filter.return_value.exists.return_value = True
|
|
||||||
|
|
||||||
mock_provider = MagicMock()
|
|
||||||
mock_provider.uid = "azure-subscription-123"
|
|
||||||
mock_provider.provider = "azure"
|
|
||||||
mock_provider_get.return_value = mock_provider
|
|
||||||
|
|
||||||
prowler_provider = MagicMock()
|
|
||||||
prowler_provider.type = "azure"
|
|
||||||
prowler_provider.identity.identity_type = "mock_identity_type"
|
|
||||||
prowler_provider.identity.identity_id = "mock_identity_id"
|
|
||||||
prowler_provider.identity.subscriptions = {
|
|
||||||
"legacy-subscription": "legacy-sub-id"
|
|
||||||
}
|
|
||||||
prowler_provider.identity.tenant_ids = ["test-ing-432a-a828-d9c965196f87"]
|
|
||||||
prowler_provider.identity.tenant_domain = "mock_tenant_domain"
|
|
||||||
prowler_provider.region_config.name = "AzureCloud"
|
|
||||||
mock_initialize_provider.return_value = prowler_provider
|
|
||||||
|
|
||||||
mock_compliance_get_bulk.return_value = {}
|
|
||||||
mock_get_available_frameworks.return_value = []
|
|
||||||
|
|
||||||
resource = MagicMock()
|
|
||||||
resource.uid = (
|
|
||||||
"/subscriptions/legacy-sub-id/providers/Microsoft.Authorization/"
|
|
||||||
"policyAssignments/legacy"
|
|
||||||
)
|
|
||||||
resource.name = "legacy-policy"
|
|
||||||
resource.region = "global"
|
|
||||||
resource.metadata = "{}"
|
|
||||||
resource.details = ""
|
|
||||||
resource.tags.all.return_value = [MagicMock(key="env", value="prod")]
|
|
||||||
|
|
||||||
dummy_finding = MagicMock()
|
|
||||||
dummy_finding.uid = "finding-uid-legacy"
|
|
||||||
dummy_finding.status = "FAIL"
|
|
||||||
dummy_finding.status_extended = "Legacy metadata finding"
|
|
||||||
dummy_finding.muted = False
|
|
||||||
dummy_finding.compliance = {}
|
|
||||||
dummy_finding.raw_result = {}
|
|
||||||
dummy_finding.check_id = (
|
|
||||||
"entra_conditional_access_policy_require_mfa_for_management_api"
|
|
||||||
)
|
|
||||||
dummy_finding.check_metadata = {
|
|
||||||
"provider": "azure",
|
|
||||||
"checkid": "entra_conditional_access_policy_require_mfa_for_management_api",
|
|
||||||
"checktitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
|
|
||||||
"checktype": [],
|
|
||||||
"servicename": "entra",
|
|
||||||
"subservicename": "",
|
|
||||||
"severity": "medium",
|
|
||||||
"resourcetype": "#microsoft.graph.conditionalAccess",
|
|
||||||
"resourcegroup": "IAM",
|
|
||||||
"description": "Legacy description",
|
|
||||||
"risk": "Legacy risk",
|
|
||||||
"relatedurl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management",
|
|
||||||
"additionalurls": [
|
|
||||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
|
|
||||||
],
|
|
||||||
"remediation": {
|
|
||||||
"code": {
|
|
||||||
"cli": "",
|
|
||||||
"other": "",
|
|
||||||
"nativeiac": "",
|
|
||||||
"terraform": "",
|
|
||||||
},
|
|
||||||
"recommendation": {
|
|
||||||
"text": "Legacy remediation",
|
|
||||||
"url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"resourceidtemplate": "",
|
|
||||||
"categories": [],
|
|
||||||
"dependson": [],
|
|
||||||
"relatedto": [],
|
|
||||||
"notes": "Legacy notes",
|
|
||||||
}
|
|
||||||
dummy_finding.resources.first.return_value = resource
|
|
||||||
|
|
||||||
mock_finding_filter.return_value.order_by.return_value.iterator.return_value = [
|
|
||||||
dummy_finding
|
|
||||||
]
|
|
||||||
|
|
||||||
writer_instances = []
|
|
||||||
|
|
||||||
def writer_factory(*args, **kwargs):
|
|
||||||
writer = MagicMock()
|
|
||||||
writer._data = []
|
|
||||||
writer.transform = MagicMock()
|
|
||||||
writer.batch_write_data_to_file = MagicMock()
|
|
||||||
writer.findings = kwargs["findings"]
|
|
||||||
writer_instances.append(writer)
|
|
||||||
return writer
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"tasks.tasks.FindingOutput._transform_findings_stats",
|
|
||||||
return_value={"some": "stats"},
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"tasks.tasks.OUTPUT_FORMATS_MAPPING",
|
|
||||||
{
|
|
||||||
"json": {
|
|
||||||
"class": writer_factory,
|
|
||||||
"suffix": ".json",
|
|
||||||
"kwargs": {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"tasks.tasks._generate_output_directory",
|
|
||||||
return_value=(
|
|
||||||
"/tmp/test/out-dir",
|
|
||||||
"/tmp/test/comp-dir",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
patch("tasks.tasks.Scan.all_objects.filter") as mock_scan_update,
|
|
||||||
patch("tasks.tasks.rmtree"),
|
|
||||||
):
|
|
||||||
mock_compress.return_value = "/tmp/zipped.zip"
|
|
||||||
mock_upload.return_value = "s3://bucket/zipped.zip"
|
|
||||||
|
|
||||||
result = generate_outputs_task(
|
|
||||||
scan_id=self.scan_id,
|
|
||||||
provider_id=self.provider_id,
|
|
||||||
tenant_id=self.tenant_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result == {"upload": True}
|
|
||||||
assert len(writer_instances) == 1
|
|
||||||
|
|
||||||
transformed_finding = writer_instances[0].findings[0]
|
|
||||||
assert transformed_finding.metadata.CheckTitle.startswith("Ensure")
|
|
||||||
assert (
|
|
||||||
transformed_finding.metadata.RelatedUrl
|
|
||||||
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management"
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
transformed_finding.metadata.Remediation.Recommendation.Url
|
|
||||||
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
|
|
||||||
)
|
|
||||||
assert transformed_finding.metadata.Severity.value == "medium"
|
|
||||||
|
|
||||||
mock_scan_update.return_value.update.assert_called_once_with(
|
|
||||||
output_location="s3://bucket/zipped.zip"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_generate_outputs_fails_upload(self):
|
def test_generate_outputs_fails_upload(self):
|
||||||
with (
|
with (
|
||||||
patch("tasks.tasks.ScanSummary.objects.filter") as mock_filter,
|
patch("tasks.tasks.ScanSummary.objects.filter") as mock_filter,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class _MutableTimestamp:
|
|||||||
|
|
||||||
timestamp = _MutableTimestamp(datetime.today())
|
timestamp = _MutableTimestamp(datetime.today())
|
||||||
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
timestamp_utc = _MutableTimestamp(datetime.now(timezone.utc))
|
||||||
prowler_version = "5.22.0"
|
prowler_version = "5.21.1"
|
||||||
html_logo_url = "https://github.com/prowler-cloud/prowler/"
|
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"
|
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"
|
aws_logo = "https://user-images.githubusercontent.com/38561120/235953920-3e3fba08-0795-41dc-b480-9bea57db9f2e.png"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from prowler.lib.check.models import (
|
|||||||
Code,
|
Code,
|
||||||
Recommendation,
|
Recommendation,
|
||||||
Remediation,
|
Remediation,
|
||||||
Severity,
|
|
||||||
)
|
)
|
||||||
from prowler.lib.logger import logger
|
from prowler.lib.logger import logger
|
||||||
from prowler.lib.outputs.common import Status, fill_common_finding_data
|
from prowler.lib.outputs.common import Status, fill_common_finding_data
|
||||||
@@ -537,74 +536,48 @@ class Finding(BaseModel):
|
|||||||
finding.zone_name = getattr(resource, "zone_name", resource.name)
|
finding.zone_name = getattr(resource, "zone_name", resource.name)
|
||||||
finding.account_id = getattr(finding, "account_id", "")
|
finding.account_id = getattr(finding, "account_id", "")
|
||||||
|
|
||||||
metadata_kwargs = cls._get_api_check_metadata_kwargs(finding.check_metadata)
|
finding.check_metadata = CheckMetadata(
|
||||||
try:
|
Provider=finding.check_metadata["provider"],
|
||||||
finding.check_metadata = CheckMetadata(**metadata_kwargs)
|
CheckID=finding.check_metadata["checkid"],
|
||||||
except ValidationError as validation_error:
|
CheckTitle=finding.check_metadata["checktitle"],
|
||||||
check_id = metadata_kwargs.get("CheckID", getattr(finding, "check_id", ""))
|
CheckType=finding.check_metadata["checktype"],
|
||||||
logger.warning(
|
ServiceName=finding.check_metadata["servicename"],
|
||||||
"Legacy persisted check metadata failed validation during API finding transformation "
|
SubServiceName=finding.check_metadata["subservicename"],
|
||||||
f"for {check_id}. Falling back to compatibility mode. Errors: {validation_error.errors()}"
|
Severity=finding.check_metadata["severity"],
|
||||||
)
|
ResourceType=finding.check_metadata["resourcetype"],
|
||||||
finding.check_metadata = cls._construct_legacy_check_metadata(
|
Description=finding.check_metadata["description"],
|
||||||
metadata_kwargs
|
Risk=finding.check_metadata["risk"],
|
||||||
)
|
RelatedUrl=finding.check_metadata["relatedurl"],
|
||||||
|
Remediation=Remediation(
|
||||||
|
Recommendation=Recommendation(
|
||||||
|
Text=finding.check_metadata["remediation"]["recommendation"][
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
Url=finding.check_metadata["remediation"]["recommendation"]["url"],
|
||||||
|
),
|
||||||
|
Code=Code(
|
||||||
|
NativeIaC=finding.check_metadata["remediation"]["code"][
|
||||||
|
"nativeiac"
|
||||||
|
],
|
||||||
|
Terraform=finding.check_metadata["remediation"]["code"][
|
||||||
|
"terraform"
|
||||||
|
],
|
||||||
|
CLI=finding.check_metadata["remediation"]["code"]["cli"],
|
||||||
|
Other=finding.check_metadata["remediation"]["code"]["other"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ResourceIdTemplate=finding.check_metadata["resourceidtemplate"],
|
||||||
|
Categories=finding.check_metadata["categories"],
|
||||||
|
DependsOn=finding.check_metadata["dependson"],
|
||||||
|
RelatedTo=finding.check_metadata["relatedto"],
|
||||||
|
Notes=finding.check_metadata["notes"],
|
||||||
|
)
|
||||||
finding.resource_tags = unroll_tags(
|
finding.resource_tags = unroll_tags(
|
||||||
[{"key": tag.key, "value": tag.value} for tag in resource.tags.all()]
|
[{"key": tag.key, "value": tag.value} for tag in resource.tags.all()]
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls.generate_output(provider, finding, SimpleNamespace())
|
return cls.generate_output(provider, finding, SimpleNamespace())
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_api_check_metadata_kwargs(check_metadata: dict) -> dict:
|
|
||||||
remediation = check_metadata["remediation"]
|
|
||||||
remediation_code = remediation["code"]
|
|
||||||
remediation_recommendation = remediation["recommendation"]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"Provider": check_metadata["provider"],
|
|
||||||
"CheckID": check_metadata["checkid"],
|
|
||||||
"CheckTitle": check_metadata["checktitle"],
|
|
||||||
"CheckType": check_metadata["checktype"],
|
|
||||||
"CheckAliases": check_metadata.get("checkaliases", []),
|
|
||||||
"ServiceName": check_metadata["servicename"],
|
|
||||||
"SubServiceName": check_metadata["subservicename"],
|
|
||||||
"Severity": check_metadata["severity"],
|
|
||||||
"ResourceType": check_metadata["resourcetype"],
|
|
||||||
"ResourceGroup": check_metadata.get("resourcegroup", ""),
|
|
||||||
"Description": check_metadata["description"],
|
|
||||||
"Risk": check_metadata["risk"],
|
|
||||||
"RelatedUrl": check_metadata["relatedurl"],
|
|
||||||
"Remediation": Remediation(
|
|
||||||
Recommendation=Recommendation(
|
|
||||||
Text=remediation_recommendation["text"],
|
|
||||||
Url=remediation_recommendation["url"],
|
|
||||||
),
|
|
||||||
Code=Code(
|
|
||||||
NativeIaC=remediation_code["nativeiac"],
|
|
||||||
Terraform=remediation_code["terraform"],
|
|
||||||
CLI=remediation_code["cli"],
|
|
||||||
Other=remediation_code["other"],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"ResourceIdTemplate": check_metadata["resourceidtemplate"],
|
|
||||||
"AdditionalURLs": check_metadata.get("additionalurls", []),
|
|
||||||
"Categories": check_metadata["categories"],
|
|
||||||
"DependsOn": check_metadata["dependson"],
|
|
||||||
"RelatedTo": check_metadata["relatedto"],
|
|
||||||
"Notes": check_metadata["notes"],
|
|
||||||
"Compliance": check_metadata.get("compliance", []),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _construct_legacy_check_metadata(metadata_kwargs: dict) -> CheckMetadata:
|
|
||||||
severity = metadata_kwargs["Severity"]
|
|
||||||
if not isinstance(severity, Severity):
|
|
||||||
severity = Severity(severity)
|
|
||||||
|
|
||||||
legacy_metadata_kwargs = {**metadata_kwargs, "Severity": severity}
|
|
||||||
return CheckMetadata.construct(**legacy_metadata_kwargs)
|
|
||||||
|
|
||||||
def _transform_findings_stats(scan_summaries: list[dict]) -> dict:
|
def _transform_findings_stats(scan_summaries: list[dict]) -> dict:
|
||||||
"""
|
"""
|
||||||
Aggregate and transform scan summary data into findings statistics.
|
Aggregate and transform scan summary data into findings statistics.
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ maintainers = [{name = "Prowler Engineering", email = "engineering@prowler.com"}
|
|||||||
name = "prowler"
|
name = "prowler"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">3.9.1,<3.13"
|
requires-python = ">3.9.1,<3.13"
|
||||||
version = "5.22.0"
|
version = "5.21.1"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
prowler = "prowler.__main__:prowler"
|
prowler = "prowler.__main__:prowler"
|
||||||
|
|||||||
@@ -1126,95 +1126,6 @@ class TestFinding:
|
|||||||
for segment in expected_segments:
|
for segment in expected_segments:
|
||||||
assert segment in finding_obj.uid
|
assert segment in finding_obj.uid
|
||||||
|
|
||||||
@patch(
|
|
||||||
"prowler.lib.outputs.finding.get_check_compliance",
|
|
||||||
new=mock_get_check_compliance,
|
|
||||||
)
|
|
||||||
def test_transform_api_finding_azure_accepts_legacy_persisted_metadata(self):
|
|
||||||
provider = MagicMock()
|
|
||||||
provider.type = "azure"
|
|
||||||
provider.identity.identity_type = "mock_identity_type"
|
|
||||||
provider.identity.identity_id = "mock_identity_id"
|
|
||||||
provider.identity.subscriptions = {"legacy-subscription": "legacy-sub-id"}
|
|
||||||
provider.identity.tenant_ids = ["test-ing-432a-a828-d9c965196f87"]
|
|
||||||
provider.identity.tenant_domain = "mock_tenant_domain"
|
|
||||||
provider.region_config.name = "AzureCloud"
|
|
||||||
|
|
||||||
api_finding = DummyAPIFinding()
|
|
||||||
api_finding.uid = "finding-uid-legacy"
|
|
||||||
api_finding.status = "FAIL"
|
|
||||||
api_finding.status_extended = "Legacy metadata finding"
|
|
||||||
api_finding.check_id = (
|
|
||||||
"entra_conditional_access_policy_require_mfa_for_management_api"
|
|
||||||
)
|
|
||||||
api_finding.check_metadata = {
|
|
||||||
"provider": "azure",
|
|
||||||
"checkid": "entra_conditional_access_policy_require_mfa_for_management_api",
|
|
||||||
"checktitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API",
|
|
||||||
"checktype": [],
|
|
||||||
"servicename": "entra",
|
|
||||||
"subservicename": "",
|
|
||||||
"severity": "medium",
|
|
||||||
"resourcetype": "#microsoft.graph.conditionalAccess",
|
|
||||||
"resourcegroup": "IAM",
|
|
||||||
"description": "Legacy description",
|
|
||||||
"risk": "Legacy risk",
|
|
||||||
"relatedurl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management",
|
|
||||||
"additionalurls": [
|
|
||||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
|
|
||||||
],
|
|
||||||
"remediation": {
|
|
||||||
"code": {
|
|
||||||
"cli": "",
|
|
||||||
"other": "",
|
|
||||||
"nativeiac": "",
|
|
||||||
"terraform": "",
|
|
||||||
},
|
|
||||||
"recommendation": {
|
|
||||||
"text": "Legacy remediation",
|
|
||||||
"url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"resourceidtemplate": "",
|
|
||||||
"categories": [],
|
|
||||||
"dependson": [],
|
|
||||||
"relatedto": [],
|
|
||||||
"notes": "Legacy notes",
|
|
||||||
}
|
|
||||||
api_finding.muted = False
|
|
||||||
api_resource = DummyResource(
|
|
||||||
uid="/subscriptions/legacy-sub-id/providers/Microsoft.Authorization/policyAssignments/legacy",
|
|
||||||
name="legacy-policy",
|
|
||||||
resource_arn="arn",
|
|
||||||
region="global",
|
|
||||||
tags=[],
|
|
||||||
)
|
|
||||||
api_finding.resources = DummyResources(api_resource)
|
|
||||||
|
|
||||||
finding_obj = Finding.transform_api_finding(api_finding, provider)
|
|
||||||
|
|
||||||
assert finding_obj.account_uid == "legacy-sub-id"
|
|
||||||
assert finding_obj.resource_uid == api_resource.uid
|
|
||||||
assert finding_obj.resource_name == api_resource.name
|
|
||||||
|
|
||||||
meta = finding_obj.metadata
|
|
||||||
assert (
|
|
||||||
meta.CheckTitle
|
|
||||||
== "Ensure Multifactor Authentication is Required for Windows Azure Service Management API"
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
meta.RelatedUrl
|
|
||||||
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management"
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
meta.Remediation.Recommendation.Url
|
|
||||||
== "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
|
|
||||||
)
|
|
||||||
assert meta.ResourceGroup == "IAM"
|
|
||||||
assert meta.AdditionalURLs == [
|
|
||||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps"
|
|
||||||
]
|
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
"prowler.lib.outputs.finding.get_check_compliance",
|
"prowler.lib.outputs.finding.get_check_compliance",
|
||||||
new=mock_get_check_compliance,
|
new=mock_get_check_compliance,
|
||||||
|
|||||||
Reference in New Issue
Block a user