mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
feat(compliance): add DORA framework for AWS (#11131)
This commit is contained in:
+2
-1
@@ -2,12 +2,13 @@
|
||||
|
||||
All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
## [1.31.0] (Prowler v5.30.0)
|
||||
## [1.31.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Automatic recovery of allowlisted idempotent background tasks whose worker died during a deploy or crash: stuck scan and summary tasks are detected and re-run instead of staying pending forever, with a `reconcile_orphan_tasks` management command for on-demand recovery [(#11416)](https://github.com/prowler-cloud/prowler/pull/11416)
|
||||
- Jira integration no longer creates duplicate issues on a retried send; findings already ticketed are skipped [(#11416)](https://github.com/prowler-cloud/prowler/pull/11416)
|
||||
- DORA compliance framework support [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
from api.models import Provider
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import (
|
||||
get_bulk_compliance_frameworks_universal,
|
||||
)
|
||||
from prowler.lib.check.models import CheckMetadata
|
||||
|
||||
AVAILABLE_COMPLIANCE_FRAMEWORKS = {}
|
||||
@@ -94,25 +96,22 @@ PROWLER_CHECKS = LazyChecksMapping()
|
||||
|
||||
|
||||
def get_compliance_frameworks(provider_type: Provider.ProviderChoices) -> list[str]:
|
||||
"""List compliance frameworks the API can load for `provider_type`.
|
||||
"""List compliance framework identifiers available for `provider_type`.
|
||||
|
||||
The list is sourced from `Compliance.get_bulk` so that the names
|
||||
returned here are guaranteed to be loadable by the bulk loader. This
|
||||
prevents downstream key mismatches (e.g. CSV report generation iterating
|
||||
framework names and looking them up in the bulk dict).
|
||||
Includes both per-provider frameworks and universal top-level frameworks
|
||||
(e.g. ``dora``, ``csa_ccm_4.0``).
|
||||
|
||||
Args:
|
||||
provider_type (Provider.ProviderChoices): The cloud provider type for which to retrieve
|
||||
available compliance frameworks (e.g., "aws", "azure", "gcp", "m365").
|
||||
provider_type (Provider.ProviderChoices): The cloud provider type
|
||||
(e.g., "aws", "azure", "gcp", "m365").
|
||||
|
||||
Returns:
|
||||
list[str]: A list of framework identifiers (e.g., "cis_1.4_aws", "mitre_attack_azure") available
|
||||
for the given provider.
|
||||
list[str]: Framework identifiers (e.g., "cis_1.4_aws", "dora").
|
||||
"""
|
||||
global AVAILABLE_COMPLIANCE_FRAMEWORKS
|
||||
if provider_type not in AVAILABLE_COMPLIANCE_FRAMEWORKS:
|
||||
AVAILABLE_COMPLIANCE_FRAMEWORKS[provider_type] = list(
|
||||
Compliance.get_bulk(provider_type).keys()
|
||||
get_bulk_compliance_frameworks_universal(provider_type).keys()
|
||||
)
|
||||
|
||||
return AVAILABLE_COMPLIANCE_FRAMEWORKS[provider_type]
|
||||
@@ -139,18 +138,14 @@ def get_prowler_provider_compliance(provider_type: Provider.ProviderChoices) ->
|
||||
"""
|
||||
Retrieve the Prowler compliance data for a specified provider type.
|
||||
|
||||
This function fetches the compliance frameworks and their associated
|
||||
requirements for the given cloud provider.
|
||||
|
||||
Args:
|
||||
provider_type (Provider.ProviderChoices): The provider type
|
||||
(e.g., 'aws', 'azure') for which to retrieve compliance data.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping compliance framework names to their respective
|
||||
Compliance objects for the specified provider.
|
||||
dict: Mapping of framework name to `ComplianceFramework` for the provider.
|
||||
"""
|
||||
return Compliance.get_bulk(provider_type)
|
||||
return get_bulk_compliance_frameworks_universal(provider_type)
|
||||
|
||||
|
||||
def _load_provider_assets(provider_type: Provider.ProviderChoices) -> tuple[dict, dict]:
|
||||
@@ -209,8 +204,8 @@ def load_prowler_checks(
|
||||
for compliance_name, compliance_data in prowler_compliance.get(
|
||||
provider_type, {}
|
||||
).items():
|
||||
for requirement in compliance_data.Requirements:
|
||||
for check in requirement.Checks:
|
||||
for requirement in compliance_data.requirements:
|
||||
for check in requirement.checks.get(provider_type, []):
|
||||
try:
|
||||
checks[provider_type][check].add(compliance_name)
|
||||
except KeyError:
|
||||
@@ -290,24 +285,40 @@ def generate_compliance_overview_template(
|
||||
requirements_status = {"passed": 0, "failed": 0, "manual": 0}
|
||||
total_requirements = 0
|
||||
|
||||
for requirement in compliance_data.Requirements:
|
||||
for requirement in compliance_data.requirements:
|
||||
total_requirements += 1
|
||||
total_checks = len(requirement.Checks)
|
||||
checks_dict = {check: None for check in requirement.Checks}
|
||||
provider_check_list = list(requirement.checks.get(provider_type, []))
|
||||
total_checks = len(provider_check_list)
|
||||
checks_dict = {check: None for check in provider_check_list}
|
||||
|
||||
req_status_val = "MANUAL" if total_checks == 0 else "PASS"
|
||||
|
||||
# MITRE attrs are wrapped under `_raw_attributes` by the
|
||||
# universal adapter — unwrap so consumers see the flat list.
|
||||
requirement_attributes = requirement.attributes
|
||||
if (
|
||||
isinstance(requirement_attributes, dict)
|
||||
and "_raw_attributes" in requirement_attributes
|
||||
):
|
||||
attributes_payload = list(requirement_attributes["_raw_attributes"])
|
||||
elif isinstance(requirement_attributes, dict):
|
||||
attributes_payload = (
|
||||
[dict(requirement_attributes)] if requirement_attributes else []
|
||||
)
|
||||
else:
|
||||
attributes_payload = [
|
||||
dict(attribute) for attribute in requirement_attributes
|
||||
]
|
||||
|
||||
# Build requirement dictionary
|
||||
requirement_dict = {
|
||||
"name": requirement.Name or requirement.Id,
|
||||
"description": requirement.Description,
|
||||
"tactics": getattr(requirement, "Tactics", []),
|
||||
"subtechniques": getattr(requirement, "SubTechniques", []),
|
||||
"platforms": getattr(requirement, "Platforms", []),
|
||||
"technique_url": getattr(requirement, "TechniqueURL", ""),
|
||||
"attributes": [
|
||||
dict(attribute) for attribute in requirement.Attributes
|
||||
],
|
||||
"name": requirement.name or requirement.id,
|
||||
"description": requirement.description,
|
||||
"tactics": requirement.tactics or [],
|
||||
"subtechniques": requirement.sub_techniques or [],
|
||||
"platforms": requirement.platforms or [],
|
||||
"technique_url": requirement.technique_url or "",
|
||||
"attributes": attributes_payload,
|
||||
"checks": checks_dict,
|
||||
"checks_status": {
|
||||
"pass": 0,
|
||||
@@ -325,15 +336,15 @@ def generate_compliance_overview_template(
|
||||
requirements_status["passed"] += 1
|
||||
|
||||
# Add requirement to compliance requirements
|
||||
compliance_requirements[requirement.Id] = requirement_dict
|
||||
compliance_requirements[requirement.id] = requirement_dict
|
||||
|
||||
# Build compliance dictionary
|
||||
compliance_dict = {
|
||||
"framework": compliance_data.Framework,
|
||||
"name": compliance_data.Name,
|
||||
"version": compliance_data.Version,
|
||||
"framework": compliance_data.framework,
|
||||
"name": compliance_data.name,
|
||||
"version": compliance_data.version,
|
||||
"provider": provider_type,
|
||||
"description": compliance_data.Description,
|
||||
"description": compliance_data.description,
|
||||
"requirements": compliance_requirements,
|
||||
"requirements_status": requirements_status,
|
||||
"total_requirements": total_requirements,
|
||||
|
||||
@@ -13137,8 +13137,59 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: CSV file containing the compliance report
|
||||
'202':
|
||||
description: The task is in progress
|
||||
'403':
|
||||
description: There is a problem with credentials
|
||||
'404':
|
||||
description: Compliance report not found
|
||||
description: Compliance report not found, or the scan has no reports yet
|
||||
/api/v1/scans/{id}/compliance/{name}/ocsf:
|
||||
get:
|
||||
operationId: scans_compliance_ocsf_retrieve
|
||||
description: Download a specific compliance report as an OCSF JSON file. Only
|
||||
universal frameworks that declare an output configuration produce this artifact
|
||||
(currently 'dora' and 'csa_ccm_4.0'); any other framework returns 404.
|
||||
summary: Retrieve compliance report as OCSF JSON
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[scan-reports]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- id
|
||||
- name
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this scan.
|
||||
required: true
|
||||
- in: path
|
||||
name: name
|
||||
schema:
|
||||
type: string
|
||||
description: The compliance report name, like 'dora'
|
||||
required: true
|
||||
tags:
|
||||
- Scan
|
||||
security:
|
||||
- JWT or API Key: []
|
||||
responses:
|
||||
'200':
|
||||
description: OCSF JSON file containing the compliance report
|
||||
'202':
|
||||
description: The task is in progress
|
||||
'403':
|
||||
description: There is a problem with credentials
|
||||
'404':
|
||||
description: Compliance report not found, the framework does not provide
|
||||
an OCSF export, or the scan has no reports yet
|
||||
/api/v1/scans/{id}/csa:
|
||||
get:
|
||||
operationId: scans_csa_retrieve
|
||||
|
||||
@@ -12,7 +12,9 @@ from api.compliance import (
|
||||
load_prowler_checks,
|
||||
)
|
||||
from api.models import Provider
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import (
|
||||
get_bulk_compliance_frameworks_universal,
|
||||
)
|
||||
|
||||
|
||||
class TestCompliance:
|
||||
@@ -28,16 +30,16 @@ class TestCompliance:
|
||||
assert set(checks) == {"check1", "check2", "check3"}
|
||||
mock_check_metadata.get_bulk.assert_called_once_with(provider_type)
|
||||
|
||||
@patch("api.compliance.Compliance")
|
||||
def test_get_prowler_provider_compliance(self, mock_compliance):
|
||||
@patch("api.compliance.get_bulk_compliance_frameworks_universal")
|
||||
def test_get_prowler_provider_compliance(self, mock_get_bulk):
|
||||
provider_type = Provider.ProviderChoices.AWS
|
||||
mock_compliance.get_bulk.return_value = {
|
||||
mock_get_bulk.return_value = {
|
||||
"compliance1": MagicMock(),
|
||||
"compliance2": MagicMock(),
|
||||
}
|
||||
compliance_data = get_prowler_provider_compliance(provider_type)
|
||||
assert compliance_data == mock_compliance.get_bulk.return_value
|
||||
mock_compliance.get_bulk.assert_called_once_with(provider_type)
|
||||
assert compliance_data == mock_get_bulk.return_value
|
||||
mock_get_bulk.assert_called_once_with(provider_type)
|
||||
|
||||
@patch("api.compliance.get_prowler_provider_checks")
|
||||
@patch("api.models.Provider.ProviderChoices")
|
||||
@@ -51,9 +53,9 @@ class TestCompliance:
|
||||
prowler_compliance = {
|
||||
"aws": {
|
||||
"compliance1": MagicMock(
|
||||
Requirements=[
|
||||
requirements=[
|
||||
MagicMock(
|
||||
Checks=["check1", "check2"],
|
||||
checks={"aws": ["check1", "check2"]},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -167,35 +169,38 @@ class TestCompliance:
|
||||
def test_generate_compliance_overview_template(self, mock_provider_choices):
|
||||
mock_provider_choices.values = ["aws"]
|
||||
|
||||
# ``name`` is a reserved MagicMock kwarg (it labels the mock for repr,
|
||||
# it does NOT set a ``.name`` attribute), so it must be assigned
|
||||
# explicitly after construction.
|
||||
requirement1 = MagicMock(
|
||||
Id="requirement1",
|
||||
Name="Requirement 1",
|
||||
Description="Description of requirement 1",
|
||||
Attributes=[],
|
||||
Checks=["check1", "check2"],
|
||||
Tactics=["tactic1"],
|
||||
SubTechniques=["subtechnique1"],
|
||||
Platforms=["platform1"],
|
||||
TechniqueURL="https://example.com",
|
||||
id="requirement1",
|
||||
description="Description of requirement 1",
|
||||
attributes=[],
|
||||
checks={"aws": ["check1", "check2"]},
|
||||
tactics=["tactic1"],
|
||||
sub_techniques=["subtechnique1"],
|
||||
platforms=["platform1"],
|
||||
technique_url="https://example.com",
|
||||
)
|
||||
requirement1.name = "Requirement 1"
|
||||
requirement2 = MagicMock(
|
||||
Id="requirement2",
|
||||
Name="Requirement 2",
|
||||
Description="Description of requirement 2",
|
||||
Attributes=[],
|
||||
Checks=[],
|
||||
Tactics=[],
|
||||
SubTechniques=[],
|
||||
Platforms=[],
|
||||
TechniqueURL="",
|
||||
id="requirement2",
|
||||
description="Description of requirement 2",
|
||||
attributes=[],
|
||||
checks={"aws": []},
|
||||
tactics=[],
|
||||
sub_techniques=[],
|
||||
platforms=[],
|
||||
technique_url="",
|
||||
)
|
||||
requirement2.name = "Requirement 2"
|
||||
compliance1 = MagicMock(
|
||||
Requirements=[requirement1, requirement2],
|
||||
Framework="Framework 1",
|
||||
Version="1.0",
|
||||
Description="Description of compliance1",
|
||||
Name="Compliance 1",
|
||||
requirements=[requirement1, requirement2],
|
||||
framework="Framework 1",
|
||||
version="1.0",
|
||||
description="Description of compliance1",
|
||||
)
|
||||
compliance1.name = "Compliance 1"
|
||||
prowler_compliance = {"aws": {"compliance1": compliance1}}
|
||||
|
||||
template = generate_compliance_overview_template(prowler_compliance)
|
||||
@@ -271,24 +276,28 @@ def reset_compliance_cache():
|
||||
|
||||
class TestGetComplianceFrameworks:
|
||||
def test_returns_keys_from_compliance_get_bulk(self, reset_compliance_cache):
|
||||
with patch("api.compliance.Compliance") as mock_compliance:
|
||||
mock_compliance.get_bulk.return_value = {
|
||||
with patch(
|
||||
"api.compliance.get_bulk_compliance_frameworks_universal"
|
||||
) as mock_get_bulk:
|
||||
mock_get_bulk.return_value = {
|
||||
"cis_1.4_aws": MagicMock(),
|
||||
"mitre_attack_aws": MagicMock(),
|
||||
}
|
||||
result = get_compliance_frameworks(Provider.ProviderChoices.AWS)
|
||||
|
||||
assert sorted(result) == ["cis_1.4_aws", "mitre_attack_aws"]
|
||||
mock_compliance.get_bulk.assert_called_once_with(Provider.ProviderChoices.AWS)
|
||||
mock_get_bulk.assert_called_once_with(Provider.ProviderChoices.AWS)
|
||||
|
||||
def test_caches_result_per_provider(self, reset_compliance_cache):
|
||||
with patch("api.compliance.Compliance") as mock_compliance:
|
||||
mock_compliance.get_bulk.return_value = {"cis_1.4_aws": MagicMock()}
|
||||
with patch(
|
||||
"api.compliance.get_bulk_compliance_frameworks_universal"
|
||||
) as mock_get_bulk:
|
||||
mock_get_bulk.return_value = {"cis_1.4_aws": MagicMock()}
|
||||
get_compliance_frameworks(Provider.ProviderChoices.AWS)
|
||||
get_compliance_frameworks(Provider.ProviderChoices.AWS)
|
||||
|
||||
# Cached after first call.
|
||||
assert mock_compliance.get_bulk.call_count == 1
|
||||
assert mock_get_bulk.call_count == 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider_type",
|
||||
@@ -296,17 +305,19 @@ class TestGetComplianceFrameworks:
|
||||
)
|
||||
def test_listing_is_subset_of_bulk(self, reset_compliance_cache, provider_type):
|
||||
"""Regression for CLOUD-API-40S: every name returned by
|
||||
``get_compliance_frameworks`` must be loadable via ``Compliance.get_bulk``.
|
||||
``get_compliance_frameworks`` must be loadable via
|
||||
``get_bulk_compliance_frameworks_universal``.
|
||||
|
||||
A divergence here is what produced ``KeyError: 'csa_ccm_4.0'`` in
|
||||
``generate_outputs_task`` after universal/multi-provider compliance
|
||||
JSONs were introduced at the top-level ``prowler/compliance/`` path.
|
||||
"""
|
||||
bulk_keys = set(Compliance.get_bulk(provider_type).keys())
|
||||
bulk_keys = set(get_bulk_compliance_frameworks_universal(provider_type).keys())
|
||||
listed = set(get_compliance_frameworks(provider_type))
|
||||
|
||||
missing = listed - bulk_keys
|
||||
assert not missing, (
|
||||
f"get_compliance_frameworks({provider_type!r}) returned names not "
|
||||
f"loadable by Compliance.get_bulk: {sorted(missing)}"
|
||||
f"loadable by get_bulk_compliance_frameworks_universal: "
|
||||
f"{sorted(missing)}"
|
||||
)
|
||||
|
||||
@@ -9560,6 +9560,16 @@ class TestComplianceOverviewViewSet:
|
||||
assert "platforms" in attributes["attributes"]["technique_details"]
|
||||
assert "technique_url" in attributes["attributes"]["technique_details"]
|
||||
|
||||
# Guard against the `_raw_attributes` wrapper leaking through —
|
||||
# the UI reads metadata[i].Category / .AWSService directly.
|
||||
metadata = attributes["attributes"]["metadata"]
|
||||
assert isinstance(metadata, list) and len(metadata) > 0
|
||||
first_attr = metadata[0]
|
||||
assert isinstance(first_attr, dict)
|
||||
assert "_raw_attributes" not in first_attr
|
||||
assert "Category" in first_attr
|
||||
assert "AWSService" in first_attr
|
||||
|
||||
def test_compliance_overview_attributes_missing_compliance_id(
|
||||
self, authenticated_client
|
||||
):
|
||||
|
||||
+105
-44
@@ -116,6 +116,7 @@ from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset
|
||||
from api.compliance import (
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE,
|
||||
get_compliance_frameworks,
|
||||
get_prowler_provider_compliance,
|
||||
)
|
||||
from api.constants import SEVERITY_ORDER
|
||||
from api.db_router import MainRouter
|
||||
@@ -1849,7 +1850,42 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet):
|
||||
200: OpenApiResponse(
|
||||
description="CSV file containing the compliance report"
|
||||
),
|
||||
404: OpenApiResponse(description="Compliance report not found"),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
403: OpenApiResponse(description="There is a problem with credentials"),
|
||||
404: OpenApiResponse(
|
||||
description="Compliance report not found, or the scan has no reports yet"
|
||||
),
|
||||
},
|
||||
request=None,
|
||||
),
|
||||
compliance_ocsf=extend_schema(
|
||||
tags=["Scan"],
|
||||
summary="Retrieve compliance report as OCSF JSON",
|
||||
description=(
|
||||
"Download a specific compliance report as an OCSF JSON file. "
|
||||
"Only universal frameworks that declare an output configuration "
|
||||
"produce this artifact (currently 'dora' and 'csa_ccm_4.0'); any "
|
||||
"other framework returns 404."
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="name",
|
||||
type=str,
|
||||
location=OpenApiParameter.PATH,
|
||||
required=True,
|
||||
description="The compliance report name, like 'dora'",
|
||||
),
|
||||
],
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="OCSF JSON file containing the compliance report"
|
||||
),
|
||||
202: OpenApiResponse(description="The task is in progress"),
|
||||
403: OpenApiResponse(description="There is a problem with credentials"),
|
||||
404: OpenApiResponse(
|
||||
description="Compliance report not found, the framework does "
|
||||
"not provide an OCSF export, or the scan has no reports yet"
|
||||
),
|
||||
},
|
||||
request=None,
|
||||
),
|
||||
@@ -1992,35 +2028,23 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
return queryset.select_related("provider", "task")
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
return ScanCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
if self.action == "partial_update":
|
||||
return ScanUpdateSerializer
|
||||
elif self.action == "report":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
return ScanReportSerializer
|
||||
elif self.action == "compliance":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
return ScanComplianceReportSerializer
|
||||
elif self.action == "threatscore":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
elif self.action == "ens":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
elif self.action == "nis2":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
elif self.action == "csa":
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
elif self.action == "cis":
|
||||
|
||||
action_defaults = {
|
||||
"create": ScanCreateSerializer,
|
||||
"report": ScanReportSerializer,
|
||||
"compliance": ScanComplianceReportSerializer,
|
||||
"compliance_ocsf": ScanComplianceReportSerializer,
|
||||
}
|
||||
response_only_actions = {"threatscore", "ens", "nis2", "csa", "cis"}
|
||||
|
||||
if self.action in action_defaults or self.action in response_only_actions:
|
||||
if hasattr(self, "response_serializer_class"):
|
||||
return self.response_serializer_class
|
||||
if self.action in action_defaults:
|
||||
return action_defaults[self.action]
|
||||
|
||||
return super().get_serializer_class()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
@@ -2269,20 +2293,16 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
content, filename = loader
|
||||
return self._serve_file(content, filename, "application/x-zip-compressed")
|
||||
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_path="compliance/(?P<name>[^/]+)",
|
||||
url_name="compliance",
|
||||
)
|
||||
def compliance(self, request, pk=None, name=None):
|
||||
scan = self.get_object()
|
||||
if name not in get_compliance_frameworks(scan.provider.provider):
|
||||
return Response(
|
||||
{"detail": f"Compliance '{name}' not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
def _serve_compliance_artifact(self, scan, name, file_extension, content_type):
|
||||
"""Resolve and serve a per-framework compliance artifact from disk/S3.
|
||||
|
||||
Shared by the CSV and OCSF compliance download actions. Both are
|
||||
path-based (no query params) on purpose: ``get_object`` runs
|
||||
``filter_queryset``, which triggers JSON:API's
|
||||
``QueryParameterValidationFilter`` and 400s on any non-JSON:API
|
||||
query param, so a ``?format=`` / ``?type=`` selector is not viable
|
||||
here — the format is encoded in the route instead.
|
||||
"""
|
||||
running_resp = self._get_task_status(scan)
|
||||
if running_resp:
|
||||
return running_resp
|
||||
@@ -2299,25 +2319,66 @@ class ScanViewSet(BaseRLSViewSet):
|
||||
bucket = env.str("DJANGO_OUTPUT_S3_AWS_OUTPUT_BUCKET", "")
|
||||
key_prefix = scan.output_location.removeprefix(f"s3://{bucket}/")
|
||||
prefix = os.path.join(
|
||||
os.path.dirname(key_prefix), "compliance", f"{name}.csv"
|
||||
os.path.dirname(key_prefix), "compliance", f"{name}.{file_extension}"
|
||||
)
|
||||
loader = self._load_file(
|
||||
prefix,
|
||||
s3=True,
|
||||
bucket=bucket,
|
||||
list_objects=True,
|
||||
content_type="text/csv",
|
||||
content_type=content_type,
|
||||
)
|
||||
else:
|
||||
base = os.path.dirname(scan.output_location)
|
||||
pattern = os.path.join(base, "compliance", f"*_{name}.csv")
|
||||
pattern = os.path.join(base, "compliance", f"*_{name}.{file_extension}")
|
||||
loader = self._load_file(pattern, s3=False)
|
||||
|
||||
if isinstance(loader, HttpResponseBase):
|
||||
return loader
|
||||
|
||||
content, filename = loader
|
||||
return self._serve_file(content, filename, "text/csv")
|
||||
return self._serve_file(content, filename, content_type)
|
||||
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_path="compliance/(?P<name>[^/]+)",
|
||||
url_name="compliance",
|
||||
)
|
||||
def compliance(self, request, pk=None, name=None):
|
||||
scan = self.get_object()
|
||||
if name not in get_compliance_frameworks(scan.provider.provider):
|
||||
return Response(
|
||||
{"detail": f"Compliance '{name}' not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
return self._serve_compliance_artifact(scan, name, "csv", "text/csv")
|
||||
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_path="compliance/(?P<name>[^/]+)/ocsf",
|
||||
url_name="compliance-ocsf",
|
||||
)
|
||||
def compliance_ocsf(self, request, pk=None, name=None):
|
||||
scan = self.get_object()
|
||||
if name not in get_compliance_frameworks(scan.provider.provider):
|
||||
return Response(
|
||||
{"detail": f"Compliance '{name}' not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
universal_bulk = get_prowler_provider_compliance(scan.provider.provider)
|
||||
framework_obj = universal_bulk.get(name)
|
||||
if not (framework_obj and getattr(framework_obj, "outputs", None)):
|
||||
return Response(
|
||||
{"detail": f"Compliance '{name}' does not provide an OCSF export."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
return self._serve_compliance_artifact(
|
||||
scan, name, "ocsf.json", "application/json"
|
||||
)
|
||||
|
||||
@action(
|
||||
detail=True,
|
||||
|
||||
@@ -39,11 +39,6 @@ from prowler.lib.outputs.compliance.cis.cis_oraclecloud import OracleCloudCIS
|
||||
from prowler.lib.outputs.compliance.cisa_scuba.cisa_scuba_googleworkspace import (
|
||||
GoogleWorkspaceCISASCuBA,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.csa.csa_alibabacloud import AlibabaCloudCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
|
||||
@@ -102,7 +97,6 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name == "prowler_threatscore_aws", ProwlerThreatScoreAWS),
|
||||
(lambda name: name.startswith("ccc_"), CCC_AWS),
|
||||
(lambda name: name.startswith("c5_"), AWSC5),
|
||||
(lambda name: name.startswith("csa_"), AWSCSA),
|
||||
(lambda name: name == "asd_essential_eight_aws", ASDEssentialEightAWS),
|
||||
],
|
||||
"azure": [
|
||||
@@ -113,7 +107,6 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name.startswith("ccc_"), CCC_Azure),
|
||||
(lambda name: name == "prowler_threatscore_azure", ProwlerThreatScoreAzure),
|
||||
(lambda name: name == "c5_azure", AzureC5),
|
||||
(lambda name: name.startswith("csa_"), AzureCSA),
|
||||
],
|
||||
"gcp": [
|
||||
(lambda name: name.startswith("cis_"), GCPCIS),
|
||||
@@ -123,7 +116,6 @@ COMPLIANCE_CLASS_MAP = {
|
||||
(lambda name: name == "prowler_threatscore_gcp", ProwlerThreatScoreGCP),
|
||||
(lambda name: name.startswith("ccc_"), CCC_GCP),
|
||||
(lambda name: name == "c5_gcp", GCPC5),
|
||||
(lambda name: name.startswith("csa_"), GCPCSA),
|
||||
],
|
||||
"kubernetes": [
|
||||
(lambda name: name.startswith("cis_"), KubernetesCIS),
|
||||
@@ -152,11 +144,9 @@ COMPLIANCE_CLASS_MAP = {
|
||||
"image": [],
|
||||
"oraclecloud": [
|
||||
(lambda name: name.startswith("cis_"), OracleCloudCIS),
|
||||
(lambda name: name.startswith("csa_"), OracleCloudCSA),
|
||||
],
|
||||
"alibabacloud": [
|
||||
(lambda name: name.startswith("cis_"), AlibabaCloudCIS),
|
||||
(lambda name: name.startswith("csa_"), AlibabaCloudCSA),
|
||||
(
|
||||
lambda name: name == "prowler_threatscore_alibabacloud",
|
||||
ProwlerThreatScoreAlibaba,
|
||||
|
||||
@@ -29,7 +29,10 @@ from api.db_router import READ_REPLICA_ALIAS, MainRouter
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Provider, Scan, ScanSummary, StateChoices, ThreatScoreSnapshot
|
||||
from api.utils import initialize_prowler_provider
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import (
|
||||
Compliance,
|
||||
get_bulk_compliance_frameworks_universal,
|
||||
)
|
||||
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
@@ -571,7 +574,7 @@ def generate_csa_report(
|
||||
Args:
|
||||
tenant_id: The tenant ID for Row-Level Security context.
|
||||
scan_id: ID of the scan executed by Prowler.
|
||||
compliance_id: ID of the compliance framework (e.g., "csa_ccm_4.0_aws").
|
||||
compliance_id: ID of the compliance framework (e.g., "csa_ccm_4.0").
|
||||
output_path: Output PDF file path.
|
||||
provider_id: Provider ID for the scan.
|
||||
only_failed: If True, only include failed requirements in detailed section.
|
||||
@@ -883,9 +886,11 @@ def generate_compliance_reports(
|
||||
frameworks_bulk.get(f"nis2_{provider_type}")
|
||||
)
|
||||
if generate_csa:
|
||||
pending_checks_by_framework["csa"] = _get_compliance_check_ids(
|
||||
frameworks_bulk.get(f"csa_ccm_4.0_{provider_type}")
|
||||
)
|
||||
# csa_ccm_4.0 lives at the top level, not under compliance/{provider}/.
|
||||
csa_framework = frameworks_bulk.get(
|
||||
"csa_ccm_4.0"
|
||||
) or get_bulk_compliance_frameworks_universal(provider_type).get("csa_ccm_4.0")
|
||||
pending_checks_by_framework["csa"] = _get_compliance_check_ids(csa_framework)
|
||||
if generate_cis and latest_cis:
|
||||
pending_checks_by_framework["cis"] = _get_compliance_check_ids(
|
||||
frameworks_bulk.get(latest_cis)
|
||||
@@ -1183,7 +1188,7 @@ def generate_compliance_reports(
|
||||
if generate_csa:
|
||||
generated_report_keys.append("csa")
|
||||
csa_path = output_paths["csa"]
|
||||
compliance_id_csa = f"csa_ccm_4.0_{provider_type}"
|
||||
compliance_id_csa = "csa_ccm_4.0"
|
||||
pdf_path_csa = f"{csa_path}_csa_report.pdf"
|
||||
logger.info("Generating CSA CCM report with compliance %s", compliance_id_csa)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import time
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from celery.utils.log import get_task_logger
|
||||
@@ -26,7 +27,10 @@ from api.db_router import READ_REPLICA_ALIAS
|
||||
from api.db_utils import rls_transaction
|
||||
from api.models import Provider, StatusChoices
|
||||
from api.utils import initialize_prowler_provider
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.compliance_models import (
|
||||
Compliance,
|
||||
get_bulk_compliance_frameworks_universal,
|
||||
)
|
||||
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||
|
||||
from .components import (
|
||||
@@ -222,6 +226,46 @@ def get_requirement_metadata(
|
||||
return None
|
||||
|
||||
|
||||
def _universal_attributes_to_list(attributes) -> list:
|
||||
"""Flatten a universal requirement's ``attributes`` into a list of objects
|
||||
with attribute access. MITRE wraps its list under ``_raw_attributes``."""
|
||||
if isinstance(attributes, dict) and "_raw_attributes" in attributes:
|
||||
entries = attributes.get("_raw_attributes") or []
|
||||
return [
|
||||
SimpleNamespace(**entry) for entry in entries if isinstance(entry, dict)
|
||||
]
|
||||
if isinstance(attributes, dict):
|
||||
return [SimpleNamespace(**attributes)] if attributes else []
|
||||
return list(attributes or [])
|
||||
|
||||
|
||||
def _adapt_universal_to_legacy(framework, provider_type: str) -> SimpleNamespace:
|
||||
"""Expose a universal ``ComplianceFramework`` under the legacy ``Compliance``
|
||||
attribute names used by the PDF pipeline."""
|
||||
provider_key = (provider_type or "").lower()
|
||||
requirements = []
|
||||
for requirement in framework.requirements:
|
||||
checks_by_provider = (
|
||||
requirement.checks if isinstance(requirement.checks, dict) else {}
|
||||
)
|
||||
requirements.append(
|
||||
SimpleNamespace(
|
||||
Id=requirement.id,
|
||||
Description=requirement.description or "",
|
||||
Checks=list(checks_by_provider.get(provider_key, [])),
|
||||
Attributes=_universal_attributes_to_list(requirement.attributes),
|
||||
)
|
||||
)
|
||||
return SimpleNamespace(
|
||||
Framework=framework.framework,
|
||||
Name=framework.name,
|
||||
Version=framework.version or "",
|
||||
Description=framework.description or "",
|
||||
Provider=framework.provider or provider_type,
|
||||
Requirements=requirements,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PDF Styles Cache
|
||||
# =============================================================================
|
||||
@@ -869,9 +913,18 @@ class BaseComplianceReportGenerator(ABC):
|
||||
prowler_provider = initialize_prowler_provider(provider_obj)
|
||||
provider_type = provider_obj.provider
|
||||
|
||||
# Load compliance framework
|
||||
frameworks_bulk = Compliance.get_bulk(provider_type)
|
||||
compliance_obj = frameworks_bulk.get(compliance_id)
|
||||
# Load compliance framework — fall back to the universal loader
|
||||
# for top-level JSONs (e.g. csa_ccm_4.0) that Compliance.get_bulk
|
||||
# does not scan.
|
||||
compliance_obj = Compliance.get_bulk(provider_type).get(compliance_id)
|
||||
if not compliance_obj:
|
||||
universal_framework = get_bulk_compliance_frameworks_universal(
|
||||
provider_type
|
||||
).get(compliance_id)
|
||||
if universal_framework:
|
||||
compliance_obj = _adapt_universal_to_legacy(
|
||||
universal_framework, provider_type
|
||||
)
|
||||
|
||||
if not compliance_obj:
|
||||
raise ValueError(f"Compliance framework not found: {compliance_id}")
|
||||
|
||||
@@ -359,35 +359,40 @@ def _load_findings_for_requirement_checks(
|
||||
def _get_compliance_check_ids(compliance_obj) -> set[str]:
|
||||
"""Return the union of all check_ids referenced by a compliance framework.
|
||||
|
||||
Used by the master report orchestrator to know which checks each
|
||||
framework consumes from the shared ``findings_cache``, so that once a
|
||||
framework finishes the entries no other pending framework needs can be
|
||||
evicted from the cache (PROWLER-1733).
|
||||
Used by the master report orchestrator to evict entries from
|
||||
``findings_cache`` once no pending framework needs them (PROWLER-1733).
|
||||
|
||||
Args:
|
||||
compliance_obj: A loaded Compliance framework object exposing a
|
||||
``Requirements`` iterable, each requirement carrying ``Checks``.
|
||||
``None`` is treated as "no checks" rather than raising, so the
|
||||
caller can pass ``frameworks_bulk.get(...)`` directly without
|
||||
an extra existence check.
|
||||
|
||||
Returns:
|
||||
Set of check_id strings (empty if ``compliance_obj`` is ``None``).
|
||||
Accepts the legacy ``Compliance`` shape (``Requirements`` / ``Checks``
|
||||
lists) and the universal ``ComplianceFramework`` shape (``requirements``
|
||||
/ ``checks`` dict keyed by provider). ``None`` returns an empty set so
|
||||
callers can pass ``frameworks_bulk.get(...)`` directly.
|
||||
"""
|
||||
if compliance_obj is None:
|
||||
return set()
|
||||
checks: set[str] = set()
|
||||
requirements = getattr(compliance_obj, "Requirements", None) or []
|
||||
|
||||
requirements = getattr(compliance_obj, "Requirements", None) or getattr(
|
||||
compliance_obj, "requirements", None
|
||||
)
|
||||
if not requirements:
|
||||
return set()
|
||||
|
||||
check_ids: set[str] = set()
|
||||
try:
|
||||
# Defensive: Mock objects (used in unit tests) return another Mock
|
||||
# for any attribute access, which is truthy but not iterable. Treat
|
||||
# any non-iterable Requirements value as "no checks".
|
||||
for req in requirements:
|
||||
req_checks = getattr(req, "Checks", None) or []
|
||||
# Mock objects in unit tests return another Mock for any attribute
|
||||
# access — truthy but not iterable. Treat that as "no checks".
|
||||
for requirement in requirements:
|
||||
requirement_checks = getattr(requirement, "Checks", None)
|
||||
if requirement_checks is None:
|
||||
checks_by_provider = getattr(requirement, "checks", None) or {}
|
||||
requirement_checks = [
|
||||
check_id
|
||||
for check_ids_list in checks_by_provider.values()
|
||||
for check_id in check_ids_list
|
||||
]
|
||||
try:
|
||||
checks.update(req_checks)
|
||||
check_ids.update(requirement_checks)
|
||||
except TypeError:
|
||||
continue
|
||||
except TypeError:
|
||||
return set()
|
||||
return checks
|
||||
return check_ids
|
||||
|
||||
@@ -68,7 +68,10 @@ from tasks.utils import (
|
||||
get_next_execution_datetime,
|
||||
)
|
||||
|
||||
from api.compliance import get_compliance_frameworks
|
||||
from api.compliance import (
|
||||
get_compliance_frameworks,
|
||||
get_prowler_provider_compliance,
|
||||
)
|
||||
from api.db_router import READ_REPLICA_ALIAS
|
||||
from api.db_utils import delete_related_daily_task, rls_transaction
|
||||
from api.decorators import handle_provider_deletion, set_tenant
|
||||
@@ -76,6 +79,9 @@ from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateC
|
||||
from api.utils import initialize_prowler_provider
|
||||
from api.v1.serializers import ScanTaskSerializer
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance import (
|
||||
process_universal_compliance_frameworks,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.generic.generic import GenericCompliance
|
||||
from prowler.lib.outputs.finding import Finding as FindingOutput
|
||||
|
||||
@@ -543,7 +549,16 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
|
||||
provider_uid = provider_obj.uid
|
||||
provider_type = provider_obj.provider
|
||||
|
||||
# Per-framework exporters in `COMPLIANCE_CLASS_MAP` consume the legacy bulk.
|
||||
frameworks_bulk = Compliance.get_bulk(provider_type)
|
||||
# Universal-only frameworks (top-level JSONs like `dora.json`) are emitted
|
||||
# via `process_universal_compliance_frameworks` below.
|
||||
universal_bulk = get_prowler_provider_compliance(provider_type)
|
||||
universal_only_names = {
|
||||
name
|
||||
for name in universal_bulk
|
||||
if name not in frameworks_bulk and universal_bulk[name].outputs
|
||||
}
|
||||
frameworks_avail = get_compliance_frameworks(provider_type)
|
||||
out_dir, comp_dir = _generate_output_directory(
|
||||
DJANGO_TMP_OUTPUT_DIRECTORY, provider_uid, tenant_id, scan_id
|
||||
@@ -568,6 +583,10 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
|
||||
|
||||
output_writers = {}
|
||||
compliance_writers = {}
|
||||
# Shared across batches so universal writers are created once and reused.
|
||||
universal_compliance_state: dict[str, list] = {"compliance": []}
|
||||
universal_base_dir = os.path.dirname(out_dir)
|
||||
universal_output_filename = os.path.basename(out_dir)
|
||||
|
||||
scan_summary = FindingOutput._transform_findings_stats(
|
||||
ScanSummary.objects.filter(scan_id=scan_id)
|
||||
@@ -622,8 +641,30 @@ def generate_outputs_task(scan_id: str, provider_id: str, tenant_id: str):
|
||||
writer.batch_write_data_to_file(**extra)
|
||||
writer._data.clear()
|
||||
|
||||
# Compliance CSVs
|
||||
# Universal-only frameworks (e.g. `dora.json`).
|
||||
if universal_only_names:
|
||||
process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks=universal_only_names,
|
||||
universal_frameworks=universal_bulk,
|
||||
finding_outputs=fos,
|
||||
output_directory=universal_base_dir,
|
||||
output_filename=universal_output_filename,
|
||||
provider=provider_type,
|
||||
generated_outputs=universal_compliance_state,
|
||||
from_cli=False,
|
||||
is_last=is_last,
|
||||
)
|
||||
|
||||
# Compliance CSVs (per-framework exporters).
|
||||
for name in frameworks_avail:
|
||||
if name in universal_only_names:
|
||||
continue
|
||||
if name not in frameworks_bulk:
|
||||
logger.warning(
|
||||
"Compliance framework '%s' missing from bulk; skipping CSV export",
|
||||
name,
|
||||
)
|
||||
continue
|
||||
compliance_obj = frameworks_bulk[name]
|
||||
|
||||
klass = GenericCompliance
|
||||
|
||||
@@ -80,7 +80,7 @@ def basic_csa_compliance_data():
|
||||
tenant_id="tenant-123",
|
||||
scan_id="scan-456",
|
||||
provider_id="provider-789",
|
||||
compliance_id="csa_ccm_4.0_aws",
|
||||
compliance_id="csa_ccm_4.0",
|
||||
framework="CSA-CCM",
|
||||
name="CSA Cloud Controls Matrix v4.0",
|
||||
version="4.0",
|
||||
|
||||
@@ -323,6 +323,7 @@ class TestGenerateOutputs:
|
||||
|
||||
mock_transformed_stats = {"some": "stats"}
|
||||
with (
|
||||
patch("tasks.tasks.get_prowler_provider_compliance", return_value={}),
|
||||
patch(
|
||||
"tasks.tasks.FindingOutput._transform_findings_stats",
|
||||
return_value=mock_transformed_stats,
|
||||
@@ -441,6 +442,7 @@ class TestGenerateOutputs:
|
||||
mock_provider.uid = "test-provider-uid"
|
||||
|
||||
with (
|
||||
patch("tasks.tasks.get_prowler_provider_compliance", return_value={}),
|
||||
patch("tasks.tasks.ScanSummary.objects.filter") as mock_filter,
|
||||
patch("tasks.tasks.Provider.objects.get", return_value=mock_provider),
|
||||
patch("tasks.tasks.initialize_prowler_provider"),
|
||||
@@ -596,6 +598,7 @@ class TestGenerateOutputs:
|
||||
]
|
||||
|
||||
with (
|
||||
patch("tasks.tasks.get_prowler_provider_compliance", return_value={}),
|
||||
patch("tasks.tasks.ScanSummary.objects.filter") as mock_summary,
|
||||
patch(
|
||||
"tasks.tasks.Provider.objects.get",
|
||||
@@ -670,6 +673,7 @@ class TestGenerateOutputs:
|
||||
mock_provider.uid = "test-provider-uid"
|
||||
|
||||
with (
|
||||
patch("tasks.tasks.get_prowler_provider_compliance", return_value={}),
|
||||
patch("tasks.tasks.ScanSummary.objects.filter") as mock_filter,
|
||||
patch("tasks.tasks.Provider.objects.get", return_value=mock_provider),
|
||||
patch("tasks.tasks.initialize_prowler_provider"),
|
||||
@@ -1113,6 +1117,7 @@ class TestCheckIntegrationsTask:
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
@patch("tasks.tasks.get_prowler_provider_compliance", return_value={})
|
||||
@patch("tasks.tasks.s3_integration_task")
|
||||
@patch("tasks.tasks.Integration.objects.filter")
|
||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
||||
@@ -1145,6 +1150,7 @@ class TestCheckIntegrationsTask:
|
||||
mock_scan_summary,
|
||||
mock_integration_filter,
|
||||
mock_s3_task,
|
||||
mock_get_prowler_compliance,
|
||||
):
|
||||
"""Test that ASFF output is generated for AWS providers with SecurityHub integration."""
|
||||
# Setup
|
||||
@@ -1241,6 +1247,7 @@ class TestCheckIntegrationsTask:
|
||||
|
||||
assert result == {"upload": True}
|
||||
|
||||
@patch("tasks.tasks.get_prowler_provider_compliance", return_value={})
|
||||
@patch("tasks.tasks.s3_integration_task")
|
||||
@patch("tasks.tasks.Integration.objects.filter")
|
||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
||||
@@ -1273,6 +1280,7 @@ class TestCheckIntegrationsTask:
|
||||
mock_scan_summary,
|
||||
mock_integration_filter,
|
||||
mock_s3_task,
|
||||
mock_get_prowler_compliance,
|
||||
):
|
||||
"""Test that ASFF output is NOT generated for AWS providers without SecurityHub integration."""
|
||||
# Setup
|
||||
@@ -1366,6 +1374,7 @@ class TestCheckIntegrationsTask:
|
||||
|
||||
assert result == {"upload": True}
|
||||
|
||||
@patch("tasks.tasks.get_prowler_provider_compliance", return_value={})
|
||||
@patch("tasks.tasks.ScanSummary.objects.filter")
|
||||
@patch("tasks.tasks.Provider.objects.get")
|
||||
@patch("tasks.tasks.initialize_prowler_provider")
|
||||
@@ -1394,6 +1403,7 @@ class TestCheckIntegrationsTask:
|
||||
mock_initialize_provider,
|
||||
mock_provider_get,
|
||||
mock_scan_summary,
|
||||
mock_get_prowler_compliance,
|
||||
):
|
||||
"""Test that ASFF output is NOT generated for non-AWS providers (e.g., Azure, GCP)."""
|
||||
# Setup
|
||||
|
||||
@@ -2,40 +2,228 @@
|
||||
title: 'Creating a New Security Compliance Framework in Prowler'
|
||||
---
|
||||
|
||||
This guide explains how to add a new security compliance framework to Prowler, end to end. It covers directory layout, the JSON schema, check mapping conventions, the Pydantic models that validate each framework, the CSV output formatter, local validation, testing, and the pull request process.
|
||||
This guide explains how to add a new security compliance framework to Prowler, end to end. It covers directory layout, the two supported JSON schemas (universal and legacy), the Pydantic models that validate each framework, check mapping conventions, output formatting, local validation, testing, and the pull request process.
|
||||
|
||||
## Introduction
|
||||
|
||||
A compliance framework in Prowler maps a public or custom control catalog (for example CIS, NIST 800-53, PCI DSS, HIPAA, ENS, CCC) to the security checks that Prowler already runs. Each requirement links to zero, one or more Prowler checks. When a scan executes, findings are aggregated per requirement to produce the compliance report rendered by Prowler CLI and Prowler Cloud.
|
||||
A compliance framework in Prowler maps a public or custom control catalog (for example CIS, NIST 800-53, PCI DSS, HIPAA, ENS, CCC, DORA) to the security checks that Prowler already runs. Each requirement links to zero, one or more Prowler checks. When a scan executes, findings are aggregated per requirement to produce the compliance report rendered by Prowler CLI and Prowler Cloud.
|
||||
|
||||
Prowler ships with 85+ compliance frameworks across All Providers. The catalog lives under `prowler/compliance/<provider>/` (or `prowler/compliance/` for universal compliance frameworks)
|
||||
Prowler ships 85+ compliance frameworks across all providers. The catalog lives under `prowler/compliance/<provider>/` (legacy, per-provider) or `prowler/compliance/` (universal, multi-provider).
|
||||
|
||||
<Warning>
|
||||
A compliance framework must represent the **complete state** of the source catalog. Every requirement defined by the framework has to be present in the JSON file, even when none of the existing Prowler checks can automate it. In that case, leave `Checks` as an empty array, but do not omit the requirement.
|
||||
A compliance framework must represent the **complete state** of the source catalog. Every requirement defined by the framework has to be present in the JSON file, even when no Prowler check can automate it. In that case, leave the requirement's check list empty, but do not omit the requirement.
|
||||
|
||||
Requirement coverage feeds the compliance percentage calculations and the metadata surfaces (dashboards, widgets, exports). Missing requirements skew those metrics and break the report as a faithful snapshot of the framework.
|
||||
</Warning>
|
||||
|
||||
### Two supported schemas
|
||||
|
||||
| Schema | When to use | File location | Discovered as |
|
||||
| --- | --- | --- | --- |
|
||||
| **Universal (recommended for new frameworks)** | Multi-provider frameworks, or single-provider frameworks that benefit from declarative table/PDF rendering | `prowler/compliance/<framework>.json` (top-level) | Available for **every** provider whose key appears in any `requirement.checks` dict |
|
||||
| **Legacy provider-specific** | Single-provider frameworks with framework-specific attribute classes already declared in the codebase (CIS, ENS, ISO 27001, etc.) | `prowler/compliance/<provider>/<framework>_<version>_<provider>.json` | Available only under that provider |
|
||||
|
||||
Auto-discovery happens in `get_bulk_compliance_frameworks_universal(provider)` (`prowler/lib/check/compliance_models.py:915`), which scans **both** the top-level `prowler/compliance/` directory and every per-provider sub-directory. Legacy frameworks are transparently converted to the universal `ComplianceFramework` model via `adapt_legacy_to_universal()` before being returned, so the rest of Prowler — CLI table rendering, CSV/OCSF outputs, PDF generation — works the same regardless of the source schema.
|
||||
|
||||
> The legacy entry-point `Compliance.get_bulk(provider)` (used by older code paths) only scans per-provider sub-directories. Universal top-level files are picked up exclusively via the universal loader; this matters if you are wiring a new code path against the legacy API.
|
||||
|
||||
For **new** frameworks, prefer the universal schema: it requires no Python code changes, supports multiple providers in a single file, and table/PDF rendering is driven entirely from declarative configuration inside the JSON.
|
||||
|
||||
> All Pydantic models in `compliance_models.py` are imported from `pydantic.v1`. Subclasses you add for the legacy schema must use `from pydantic.v1 import BaseModel`.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before adding a new framework, complete the following checks:
|
||||
|
||||
- **Verify the framework is not already supported.** Inspect `prowler/compliance/<provider>/` for an existing JSON file matching the name and version.
|
||||
- **Verify the framework is not already supported.** Inspect `prowler/compliance/` and every `prowler/compliance/<provider>/` for an existing JSON file matching the name and version.
|
||||
- **Confirm the required checks exist.** Every requirement that can be automated must point to one or more existing Prowler checks. For each missing check, implement it first by following the [Prowler Checks](/developer-guide/checks) guide.
|
||||
- **Review a reference framework.** Use an existing framework with a similar structure as your template. `cis_2.0_aws.json` is the canonical reference for CIS-style frameworks. `ccc_aws.json`, `ens_rd2022_aws.json`, and `nist_800_53_revision_5_aws.json` illustrate other attribute shapes.
|
||||
- **Review a reference framework.** Use an existing framework with a similar structure as your template:
|
||||
- Universal: `prowler/compliance/dora.json`, `prowler/compliance/csa_ccm_4.0.json`.
|
||||
- Legacy: `prowler/compliance/aws/cis_2.0_aws.json` (canonical CIS shape), `prowler/compliance/aws/ccc_aws.json`, `prowler/compliance/aws/ens_rd2022_aws.json`, `prowler/compliance/aws/nist_800_53_revision_5_aws.json`.
|
||||
|
||||
## Four-Layer Architecture
|
||||
## Universal Compliance Framework
|
||||
|
||||
A compliance framework spans four layers. A complete contribution must touch each layer that applies.
|
||||
### Where the file lives
|
||||
|
||||
- **Layer 1 – Schema validation:** The Pydantic models in `prowler/lib/check/compliance_models.py` define the canonical schema for each attribute shape (CIS, ENS, Mitre, CCC, C5, CSA CCM, ISO 27001, KISA ISMS-P, AWS Well-Architected, Prowler ThreatScore, and a generic fallback).
|
||||
- **Layer 2 – JSON catalog:** The framework JSON file in `prowler/compliance/<provider>/` lists every requirement and maps it to checks.
|
||||
- **Layer 3 – Output formatter:** The Python module in `prowler/lib/outputs/compliance/<framework>/` builds the CSV row model, the per-provider transformer, and the CLI summary table.
|
||||
- **Layer 4 – Output dispatchers:** The dispatchers in `prowler/lib/outputs/compliance/compliance.py` and `prowler/lib/outputs/compliance/compliance_output.py` route findings to the right formatter based on the framework identifier.
|
||||
Place the file at the top level of the compliance directory:
|
||||
|
||||
The rest of this guide walks each layer in order.
|
||||
```
|
||||
prowler/compliance/<framework_name>.json
|
||||
```
|
||||
|
||||
## Directory Structure and File Naming
|
||||
Examples in the repository: `prowler/compliance/csa_ccm_4.0.json`, `prowler/compliance/dora.json`.
|
||||
|
||||
The file is auto-discovered — there is **no** need to register it in any `__init__.py`, modify `prowler/lib/outputs/`, or update any other Python module. The framework key Prowler CLI accepts via `--compliance` is the basename of the JSON file without `.json` (`dora.json` → `dora`).
|
||||
|
||||
### Top-level structure
|
||||
|
||||
```json
|
||||
{
|
||||
"framework": "<short identifier, e.g. \"DORA\" or \"CSA-CCM\">",
|
||||
"name": "<human-readable full name>",
|
||||
"version": "<framework version>",
|
||||
"description": "<one-paragraph description shown in --list-compliance and PDF reports>",
|
||||
"icon": "<short icon slug, optional>",
|
||||
"attributes_metadata": [ /* see below */ ],
|
||||
"outputs": { /* see below — optional */ },
|
||||
"requirements": [ /* see below */ ]
|
||||
}
|
||||
```
|
||||
|
||||
A `provider` field at the top level is **optional**. The framework's effective provider list is derived by `ComplianceFramework.get_providers()` (`compliance_models.py:739`) from the union of all keys appearing in `requirement.checks` across all requirements; the explicit `provider` field is used **only as a fallback** when no requirement carries any `checks` key. This is what enables a single file (e.g. `dora.json`) to cover AWS today and add Azure / GCP / etc. tomorrow without restructuring.
|
||||
|
||||
Provider keys inside `requirement.checks` must match the directory names under `prowler/providers/`. The valid keys at present are: `aws`, `azure`, `gcp`, `m365`, `kubernetes`, `iac`, `github`, `googleworkspace`, `alibabacloud`, `cloudflare`, `mongodbatlas`, `nhn`, `openstack`, `oraclecloud`, `llm`. Comparison in `supports_provider()` is case-insensitive, but lowercase is the convention used everywhere in the repository.
|
||||
|
||||
### `attributes_metadata`
|
||||
|
||||
Declares the shape of the per-requirement `attributes` dict. When this field is present, the root validator `validate_attributes_against_metadata` (`compliance_models.py:669`) enforces the schema at load time and rejects:
|
||||
|
||||
- Missing keys marked `required: true`.
|
||||
- Keys present in `attributes` but not declared in `attributes_metadata` (typo / drift guard).
|
||||
- Values that violate a declared `enum`.
|
||||
- Values whose Python type does not match a declared `int`, `float` or `bool`.
|
||||
|
||||
The runtime type check **only** covers `int`, `float` and `bool`. For `str`, `list_str` and `list_dict` the type is documentation-only — non-conforming values won't fail validation. If `attributes_metadata` is omitted, **no per-requirement validation runs at all**.
|
||||
|
||||
```json
|
||||
"attributes_metadata": [
|
||||
{
|
||||
"key": "Pillar",
|
||||
"label": "Pillar",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"enum": [
|
||||
"ICT Risk Management",
|
||||
"ICT-Related Incident Reporting",
|
||||
"Digital Operational Resilience Testing",
|
||||
"ICT Third-Party Risk Management",
|
||||
"Information Sharing"
|
||||
],
|
||||
"output_formats": { "csv": true, "ocsf": true }
|
||||
},
|
||||
{
|
||||
"key": "Article",
|
||||
"label": "Article",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": { "csv": true, "ocsf": true }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Per attribute:
|
||||
|
||||
- `key` (required): attribute name as it will appear in `requirement.attributes`.
|
||||
- `label`: human-readable label used in CSV headers and PDF.
|
||||
- `type`: one of `str`, `int`, `float`, `bool`, `list_str`, `list_dict`. Defaults to `str`.
|
||||
- `enum`: optional list of allowed values; non-conforming values are rejected at load time.
|
||||
- `required`: if `true`, every requirement must include this key with a non-null value.
|
||||
- `enum_display` / `enum_order`: optional per-enum-value visual metadata (label, abbreviation, color, icon) and explicit ordering for PDF rendering.
|
||||
- `output_formats`: `{ "csv": <bool>, "ocsf": <bool> }` — toggles inclusion in each output format. Both default to `true`.
|
||||
|
||||
### `outputs`
|
||||
|
||||
Optional. Controls how the framework is rendered in the console table and in the generated PDF report. Skipping it falls back to sensible defaults.
|
||||
|
||||
```json
|
||||
"outputs": {
|
||||
"table_config": {
|
||||
"group_by": "Pillar"
|
||||
},
|
||||
"pdf_config": {
|
||||
"language": "en",
|
||||
"primary_color": "#003399",
|
||||
"secondary_color": "#0055A5",
|
||||
"bg_color": "#F0F4FA",
|
||||
"group_by_field": "Pillar",
|
||||
"sections": [ "ICT Risk Management", "ICT-Related Incident Reporting", "..." ],
|
||||
"section_short_names": { "ICT Risk Management": "ICT Risk Mgmt" },
|
||||
"charts": [
|
||||
{
|
||||
"id": "pillar_compliance",
|
||||
"type": "horizontal_bar",
|
||||
"group_by": "Pillar",
|
||||
"title": "Compliance Score by Pillar",
|
||||
"y_label": "Pillar",
|
||||
"x_label": "Compliance %",
|
||||
"value_source": "compliance_percent",
|
||||
"color_mode": "by_value"
|
||||
}
|
||||
],
|
||||
"filter": { "only_failed": true, "include_manual": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`table_config.group_by` must reference an attribute key declared in `attributes_metadata`. The same applies to `pdf_config.group_by_field` and to every `charts[].group_by`.
|
||||
|
||||
For frameworks with weighted scoring (e.g. ThreatScore) declare `pdf_config.scoring` with `risk_field` / `weight_field` / `risk_boost_factor`. For column splitting (e.g. CIS Level 1 vs Level 2) use `table_config.split_by`.
|
||||
|
||||
### `requirements`
|
||||
|
||||
```json
|
||||
"requirements": [
|
||||
{
|
||||
"id": "DORA-Art5",
|
||||
"name": "Governance and organisation",
|
||||
"description": "Financial entities shall have a sound, comprehensive and well-documented ICT internal governance and control framework. ...",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 5",
|
||||
"ArticleTitle": "Governance and organisation"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_avoid_root_usage",
|
||||
"iam_no_root_access_key",
|
||||
"iam_root_mfa_enabled"
|
||||
],
|
||||
"azure": [],
|
||||
"gcp": []
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Per requirement:
|
||||
|
||||
- `id` (required): unique identifier within the framework.
|
||||
- `description` (required): the requirement text as authored by the framework.
|
||||
- `name`: short title shown alongside the id.
|
||||
- `attributes`: flat dict; keys must conform to `attributes_metadata`.
|
||||
- `checks`: dict keyed by provider name (the same lowercase keys listed in the previous section). Each value is a list of Prowler check names that evidence this requirement for that provider. The list **may be empty** and the dict itself defaults to `{}` if omitted; either way the requirement is still loaded and listed by `--list-compliance-requirements`, it just has zero checks to execute. Note: there is **no automatic check-existence validation** at load time — referencing a non-existent check name will silently produce a requirement with no findings. Validate this yourself (see "Validating Your Framework" below).
|
||||
|
||||
For MITRE-style frameworks, additional optional fields are available on the requirement: `tactics`, `sub_techniques`, `platforms`, `technique_url` (these are populated automatically when adapting a legacy MITRE JSON to the universal model).
|
||||
|
||||
### Multi-provider frameworks
|
||||
|
||||
A single universal file can cover any number of providers. The framework appears under each provider's `--list-compliance` output as long as **at least one** requirement has that provider key in its `checks` dict.
|
||||
|
||||
When extending an existing universal framework with a new provider, the only change required is editing `requirement.checks`:
|
||||
|
||||
```diff
|
||||
"checks": {
|
||||
"aws": ["iam_avoid_root_usage", "iam_no_root_access_key"],
|
||||
+ "azure": ["entra_policy_ensure_mfa_for_admin_roles"]
|
||||
}
|
||||
```
|
||||
|
||||
No code changes, no new file, no registration step.
|
||||
|
||||
## Legacy Provider-Specific Compliance Framework
|
||||
|
||||
The legacy schema is still fully supported and remains the format used by most frameworks shipped today (CIS, NIST, ISO 27001, FedRAMP, PCI DSS, GDPR, HIPAA, ENS, etc.). It binds a framework to a single provider and validates each requirement against a framework-specific Pydantic attribute class.
|
||||
|
||||
The legacy schema spans **four layers** — a complete contribution must touch every layer that applies:
|
||||
|
||||
- **Layer 1 — Schema validation:** the Pydantic models in `prowler/lib/check/compliance_models.py` define the canonical schema for each attribute shape.
|
||||
- **Layer 2 — JSON catalog:** the framework JSON file in `prowler/compliance/<provider>/` lists every requirement and maps it to checks.
|
||||
- **Layer 3 — Output formatter:** the Python module in `prowler/lib/outputs/compliance/<framework>/` builds the CSV row model, the per-provider transformer, and the CLI summary table.
|
||||
- **Layer 4 — Output dispatchers:** the dispatchers in `prowler/lib/outputs/compliance/compliance.py` and `prowler/lib/outputs/compliance/compliance_output.py` route findings to the right formatter based on the framework identifier.
|
||||
|
||||
The universal schema collapses Layers 3 and 4 into declarative configuration inside the JSON — that is the main reason it is preferred for new contributions.
|
||||
|
||||
### Directory structure and file naming
|
||||
|
||||
Compliance frameworks live at:
|
||||
|
||||
@@ -46,8 +234,8 @@ prowler/compliance/<provider>/<framework>_<version>_<provider>.json
|
||||
The filename conventions are:
|
||||
|
||||
- All lowercase, words separated with underscores.
|
||||
- `<provider>` is a supported provider identifier: `aws`, `azure`, `gcp`, `kubernetes`, `m365`, `github`, `googleworkspace`, `alibabacloud`, `oraclecloud`, `cloudflare`, `mongodbatlas`, `nhn`, `openstack`, `iac`, `llm`.
|
||||
- `<version>` is optional. Omit it when the framework has no versioning, as in `ccc_aws.json`.
|
||||
- `<provider>` is a supported provider identifier (same lowercase list as the universal section above).
|
||||
- `<version>` is optional but recommended. Omit only when the framework has no versioning (e.g. `ccc_aws.json`).
|
||||
- The file basename (without `.json`) is the framework key that Prowler CLI accepts via `--compliance`.
|
||||
|
||||
Examples:
|
||||
@@ -62,48 +250,50 @@ The output formatter directory mirrors the framework name:
|
||||
|
||||
```
|
||||
prowler/lib/outputs/compliance/<framework>/
|
||||
├── <framework>.py # CLI summary-table dispatcher
|
||||
├── <framework>.py # CLI summary-table dispatcher
|
||||
├── <framework>_<provider>.py # Per-provider transformer class
|
||||
├── models.py # Pydantic CSV row model
|
||||
└── __init__.py
|
||||
```
|
||||
|
||||
## JSON Schema Reference
|
||||
### JSON schema reference
|
||||
|
||||
Every compliance file is a JSON document with the following top-level keys.
|
||||
Every legacy compliance file is a JSON document with the following top-level keys. `Framework`, `Name` and `Provider` are validated non-empty by the root validator `framework_and_provider_must_not_be_empty` (`compliance_models.py:329`).
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `Framework` | string | Yes | Canonical framework identifier, for example `CIS`, `NIST-800-53-Revision-5`, `ENS`, `CCC`. |
|
||||
| `Name` | string | Yes | Human-readable framework name displayed by Prowler App. |
|
||||
| `Version` | string | Yes | Framework version, for example `2.0`. Use an empty string only for frameworks without versioning. See [Version Handling](#version-handling). |
|
||||
| `Version` | string | Yes (recommended) | Framework version, e.g. `2.0`. See [Version Handling](#version-handling). |
|
||||
| `Provider` | string | Yes | Upper-cased provider identifier: `AWS`, `AZURE`, `GCP`, `KUBERNETES`, `M365`, `GITHUB`, `GOOGLEWORKSPACE`, and so on. |
|
||||
| `Description` | string | Yes | Short description of the framework's scope and purpose. |
|
||||
| `Requirements` | array | Yes | List of [requirement objects](#requirement-object). |
|
||||
|
||||
### Requirement Object
|
||||
#### Requirement Object
|
||||
|
||||
Each entry in `Requirements` describes one control or requirement.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `Id` | string | Yes | Unique identifier within the framework, for example `1.10` or `CCC.Core.CN01.AR01`. |
|
||||
| `Name` | string | No | Optional human-readable name used by frameworks that distinguish control name from description, such as NIST. |
|
||||
| `Name` | string | No | Optional human-readable name (frameworks like NIST distinguish control name from description). |
|
||||
| `Description` | string | Yes | Verbatim description from the source framework. |
|
||||
| `Attributes` | array | Yes | List of [attribute objects](#attribute-objects). The shape depends on the framework. |
|
||||
| `Checks` | array of strings | Yes | Prowler check identifiers that automate the requirement. Leave the list empty when the control cannot be automated. |
|
||||
|
||||
### Attribute Objects
|
||||
#### Attribute Objects
|
||||
|
||||
Attributes carry the metadata that Prowler App and the CSV output display for each requirement. The object shape is framework-specific and is validated by a dedicated Pydantic model in `prowler/lib/check/compliance_models.py`. The most common shapes are summarized below.
|
||||
`Attributes` is parsed against the union declared in `Compliance_Requirement.Attributes` (`compliance_models.py:293`). Pydantic v1 tries each member of the union in declaration order and falls back to `Generic_Compliance_Requirement_Attribute` (the last entry) when nothing else matches — so a brand-new shape that doesn't match any existing class will silently be accepted as Generic, losing its specific fields.
|
||||
|
||||
#### CIS_Requirement_Attribute
|
||||
As of today, the registered attribute classes are: `CIS_Requirement_Attribute`, `ENS_Requirement_Attribute`, `ASDEssentialEight_Requirement_Attribute`, `ISO27001_2013_Requirement_Attribute`, `AWS_Well_Architected_Requirement_Attribute`, `KISA_ISMSP_Requirement_Attribute`, `Prowler_ThreatScore_Requirement_Attribute`, `CCC_Requirement_Attribute`, `C5Germany_Requirement_Attribute`, `CSA_CCM_Requirement_Attribute`, and `Generic_Compliance_Requirement_Attribute` (fallback). MITRE-style frameworks use the separate `Mitre_Requirement` model with `Tactics` / `SubTechniques` / `Platforms` / `TechniqueURL` at the requirement top level. The most common shapes are summarized below.
|
||||
|
||||
##### CIS_Requirement_Attribute
|
||||
|
||||
Used by every CIS benchmark.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `Section` | string | Yes | Top-level section, for example `1 Identity and Access Management`. |
|
||||
| `Section` | string | Yes | Top-level section, e.g. `1 Identity and Access Management`. |
|
||||
| `SubSection` | string | No | Optional second-level grouping. |
|
||||
| `Profile` | enum | Yes | One of `Level 1`, `Level 2`, `E3 Level 1`, `E3 Level 2`, `E5 Level 1`, `E5 Level 2`. |
|
||||
| `AssessmentStatus` | enum | Yes | `Manual` or `Automated`. |
|
||||
@@ -116,7 +306,7 @@ Used by every CIS benchmark.
|
||||
| `DefaultValue` | string | No | Default configuration value, when relevant. |
|
||||
| `References` | string | Yes | Colon-separated list of reference URLs. |
|
||||
|
||||
#### ENS_Requirement_Attribute
|
||||
##### ENS_Requirement_Attribute
|
||||
|
||||
Used by the Spanish ENS (Esquema Nacional de Seguridad) frameworks.
|
||||
|
||||
@@ -132,13 +322,13 @@ Used by the Spanish ENS (Esquema Nacional de Seguridad) frameworks.
|
||||
| `ModoEjecucion` | string | Yes | Execution mode (`manual`, `automático`, `híbrido`). |
|
||||
| `Dependencias` | array of strings | Yes | Ids of prerequisite controls. Empty list when none. |
|
||||
|
||||
#### CCC_Requirement_Attribute
|
||||
##### CCC_Requirement_Attribute
|
||||
|
||||
Used by the Common Cloud Controls Catalog.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `FamilyName` | string | Yes | Control family, for example `Data`. |
|
||||
| `FamilyName` | string | Yes | Control family, e.g. `Data`. |
|
||||
| `FamilyDescription` | string | Yes | Description of the family. |
|
||||
| `Section` | string | Yes | Section title. |
|
||||
| `SubSection` | string | Yes | Subsection title, or empty string. |
|
||||
@@ -148,9 +338,9 @@ Used by the Common Cloud Controls Catalog.
|
||||
| `SectionThreatMappings` | array of objects | Yes | Each entry has `ReferenceId` and `Identifiers`. |
|
||||
| `SectionGuidelineMappings` | array of objects | Yes | Each entry has `ReferenceId` and `Identifiers`. |
|
||||
|
||||
#### Generic_Compliance_Requirement_Attribute
|
||||
##### Generic_Compliance_Requirement_Attribute
|
||||
|
||||
The fallback attribute model used when no framework-specific schema applies (for example NIST 800-53, PCI DSS, GDPR, HIPAA).
|
||||
The fallback attribute model used when no framework-specific schema applies (e.g. NIST 800-53, PCI DSS, GDPR, HIPAA). It is **always the last** element of the `Compliance_Requirement.Attributes` Union; that ordering is load-bearing.
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
@@ -158,17 +348,17 @@ The fallback attribute model used when no framework-specific schema applies (for
|
||||
| `Section` | string | No | Section name. |
|
||||
| `SubSection` | string | No | Subsection name. |
|
||||
| `SubGroup` | string | No | Subgroup name. |
|
||||
| `Service` | string | No | Affected service, for example `aws`, `iam`. |
|
||||
| `Service` | string | No | Affected service, e.g. `iam`. |
|
||||
| `Type` | string | No | Control type. |
|
||||
| `Comment` | string | No | Free-form comment. |
|
||||
|
||||
Additional per-framework attribute models exist for `AWS_Well_Architected_Requirement_Attribute`, `ISO27001_2013_Requirement_Attribute`, `Mitre_Requirement_Attribute_<Provider>`, `KISA_ISMSP_Requirement_Attribute`, `Prowler_ThreatScore_Requirement_Attribute`, `C5Germany_Requirement_Attribute`, and `CSA_CCM_Requirement_Attribute`. Consult `prowler/lib/check/compliance_models.py` for their full field sets.
|
||||
For the remaining attribute classes (`AWS_Well_Architected_Requirement_Attribute`, `ISO27001_2013_Requirement_Attribute`, `Mitre_Requirement_Attribute_<Provider>`, `KISA_ISMSP_Requirement_Attribute`, `Prowler_ThreatScore_Requirement_Attribute`, `C5Germany_Requirement_Attribute`, `CSA_CCM_Requirement_Attribute`) consult `prowler/lib/check/compliance_models.py` for the full field sets.
|
||||
|
||||
<Note>
|
||||
The `Attributes` field is a Pydantic `Union`. The generic attribute model must remain the last element of that Union, otherwise Pydantic v1 silently coerces every framework into the generic shape and your specialized fields are dropped.
|
||||
The `Attributes` field is a Pydantic `Union`. The generic attribute model **must** remain the last element of that Union — otherwise Pydantic v1 silently coerces every framework into the generic shape and your specialized fields are dropped. Adding a brand-new attribute shape requires inserting the Pydantic class **before** `Generic_Compliance_Requirement_Attribute`.
|
||||
</Note>
|
||||
|
||||
## Minimal Working Example
|
||||
#### Minimal working example
|
||||
|
||||
The following snippet is a complete, valid framework file named `my_framework_1.0_aws.json`, saved at `prowler/compliance/aws/my_framework_1.0_aws.json`. It uses the generic attribute shape for simplicity.
|
||||
|
||||
@@ -214,26 +404,26 @@ The following snippet is a complete, valid framework file named `my_framework_1.
|
||||
}
|
||||
```
|
||||
|
||||
## Mapping Checks to Requirements
|
||||
### Mapping checks to requirements
|
||||
|
||||
Each requirement links to the Prowler checks that, together, produce a PASS or FAIL verdict for that control.
|
||||
|
||||
- **Include every requirement from the source catalog.** The framework file must mirror the full control list, one-to-one. Compliance percentages, dashboards, and exported metadata are computed against the total requirement count, so omitting an unmappable control inflates coverage and misrepresents the framework.
|
||||
- List every check by its canonical identifier, the value of `CheckID` inside the check's `.metadata.json` file.
|
||||
- **Include every requirement from the source catalog.** The framework file must mirror the full control list, one-to-one. Compliance percentages, dashboards, and exported metadata are computed against the total requirement count.
|
||||
- List every check by its canonical identifier — the value of `CheckID` inside the check's `.metadata.json` file.
|
||||
- One requirement can reference multiple checks. The requirement is evaluated as FAIL when any referenced check produces a FAIL finding for a resource in scope.
|
||||
- Leave `Checks` as an empty array when the requirement cannot be automated. The requirement still appears in the report, contributes to the total, and resolves to `MANUAL`. An empty mapping is valid; a missing requirement is not.
|
||||
- Leave `Checks` (legacy) or `checks.<provider>` (universal) as an empty array when the requirement cannot be automated. The requirement still appears in the report and contributes to the total.
|
||||
- Reuse checks across requirements when the same control applies in multiple places. Do not duplicate check logic to match framework structure.
|
||||
- Avoid referencing checks from a different provider. A compliance file is bound to one provider, and cross-provider checks will never match findings in the scan.
|
||||
- Avoid referencing checks from a different provider. A legacy compliance file is bound to one provider, and cross-provider checks will never match findings in the scan.
|
||||
|
||||
To discover available checks, run:
|
||||
To discover available checks:
|
||||
|
||||
```bash
|
||||
uv run python prowler-cli.py <provider> --list-checks
|
||||
```
|
||||
|
||||
## Supporting Multiple Providers
|
||||
### Supporting multiple providers (legacy)
|
||||
|
||||
Each compliance file targets a single provider. To cover several providers with the same framework (for example CIS across AWS, Azure, and GCP), ship one JSON file per provider:
|
||||
The legacy schema binds each file to a single provider. To cover several providers with the same framework, ship one JSON file per provider:
|
||||
|
||||
```
|
||||
prowler/compliance/aws/cis_2.0_aws.json
|
||||
@@ -241,15 +431,15 @@ prowler/compliance/azure/cis_2.0_azure.json
|
||||
prowler/compliance/gcp/cis_2.0_gcp.json
|
||||
```
|
||||
|
||||
Keep the `Framework` and `Version` values identical across the files so the dispatcher matches them, and change only the `Provider`, `Checks`, and provider-specific metadata.
|
||||
Keep the `Framework` and `Version` values identical across the files so the dispatcher matches them; change only the `Provider`, `Checks`, and provider-specific metadata. The CIS output formatter already supports every provider listed above.
|
||||
|
||||
The CIS output formatter already supports every provider listed above. For a brand-new framework that spans several providers, add one transformer per provider in `prowler/lib/outputs/compliance/<framework>/` and extend the summary-table dispatcher accordingly. See [Output Formatter](#output-formatter).
|
||||
For a brand-new framework that spans several providers, **prefer the universal schema** — it covers every provider from a single file. If you must use the legacy schema, add one transformer per provider in `prowler/lib/outputs/compliance/<framework>/` and extend the summary-table dispatcher accordingly. See [Output Formatter](#output-formatter).
|
||||
|
||||
## Output Formatter
|
||||
### Output formatter
|
||||
|
||||
Prowler renders every compliance framework in two forms: a detailed CSV report written to disk, and a summary table printed in the CLI. Both are produced by the output formatter package for the framework.
|
||||
Legacy frameworks render in two forms: a detailed CSV report written to disk, and a summary table printed in the CLI. Both are produced by the output formatter package for the framework. Universal frameworks do **not** need a Python output formatter — the `outputs` config inside the JSON drives rendering — so this section applies only to the legacy schema.
|
||||
|
||||
For a new framework named `my_framework`, create:
|
||||
For a new legacy framework named `my_framework`, create:
|
||||
|
||||
```
|
||||
prowler/lib/outputs/compliance/my_framework/
|
||||
@@ -259,19 +449,19 @@ prowler/lib/outputs/compliance/my_framework/
|
||||
└── models.py # CSV row Pydantic model
|
||||
```
|
||||
|
||||
### Step 1 – Define the CSV Row Model
|
||||
#### Step 1 — Define the CSV row model
|
||||
|
||||
In `models.py`, declare a Pydantic v1 model with one field per CSV column. Use existing models such as `AWSCISModel` in `prowler/lib/outputs/compliance/cis/models.py` as the reference. Fields typically include `Provider`, `Description`, `AccountId`, `Region`, `AssessmentDate`, `Requirements_Id`, `Requirements_Description`, one `Requirements_Attributes_*` field per attribute key, plus the finding fields `Status`, `StatusExtended`, `ResourceId`, `ResourceName`, `CheckId`, `Muted`, `Framework`, `Name`.
|
||||
|
||||
### Step 2 – Implement the Transformer Class
|
||||
#### Step 2 — Implement the transformer
|
||||
|
||||
In `my_framework_aws.py`, subclass `ComplianceOutput` from `prowler.lib.outputs.compliance.compliance_output` and implement `transform(findings, compliance, compliance_name)`. Iterate over `findings`, match each finding to the requirements it satisfies through `finding.compliance.get(compliance_name, [])`, and append one row per attribute to `self._data`.
|
||||
|
||||
### Step 3 – Add the Summary-Table Dispatcher
|
||||
#### Step 3 — Add the summary-table dispatcher
|
||||
|
||||
In `my_framework.py`, implement `get_my_framework_table(findings, bulk_checks_metadata, compliance_framework, output_filename, output_directory, compliance_overview)` following the pattern in `prowler/lib/outputs/compliance/cis/cis.py`.
|
||||
|
||||
### Step 4 – Register the Framework in the Dispatchers
|
||||
#### Step 4 — Register the framework in the dispatchers
|
||||
|
||||
- Add the dispatcher call in `prowler/lib/outputs/compliance/compliance.py`, inside `display_compliance_table`, with a branch such as `elif "my_framework" in compliance_framework:`.
|
||||
- Register the CSV model and transformer in `prowler/lib/outputs/compliance/compliance_output.py` so the CSV file is emitted during the scan.
|
||||
@@ -280,49 +470,94 @@ In `my_framework.py`, implement `get_my_framework_table(findings, bulk_checks_me
|
||||
For NIST-style catalogs that use `Generic_Compliance_Requirement_Attribute`, no custom formatter is needed. The generic formatter in `prowler/lib/outputs/compliance/generic/` handles them automatically, provided the JSON validates against the generic attribute schema.
|
||||
</Note>
|
||||
|
||||
## Version Handling
|
||||
### Legacy-to-universal adapter
|
||||
|
||||
At load time, every legacy file is transparently adapted to a `ComplianceFramework` via `adapt_legacy_to_universal()` (`compliance_models.py:819`), which: (a) flattens the first element of `Attributes` into a flat `attributes` dict, (b) wraps `Checks` as `{provider_lower: [...]}`, (c) infers `attributes_metadata` from the matched Pydantic class via `_infer_attribute_metadata()`. The rest of Prowler (CSV/OCSF/PDF output, CLI table) then treats both formats identically.
|
||||
|
||||
Loader-error behaviour differs between the two entry points:
|
||||
|
||||
- `load_compliance_framework()` (legacy) is **fail-fast**: it calls `sys.exit(1)` on any `ValidationError` (`compliance_models.py:464`).
|
||||
- `load_compliance_framework_universal()` is more lenient — it logs the error and returns `None`, so `get_bulk_compliance_frameworks_universal()` simply skips the broken file and keeps loading the rest.
|
||||
|
||||
## Version handling
|
||||
|
||||
Prowler matches frameworks by concatenating `Framework` and `Version`. A missing or empty `Version` collapses several frameworks to the same key and breaks CLI filtering with `--compliance`.
|
||||
|
||||
- Always set `Version` to a non-empty string, even for frameworks that rename editions rather than version them. Use the edition identifier (for example `RD2022`, `v2025.10`, `4.0`).
|
||||
- Always set `Version` (or `version` for universal frameworks) to a non-empty string, even for frameworks that rename editions rather than version them. Use the edition identifier (for example `RD2022`, `v2025.10`, `4.0`, `2022/2554`).
|
||||
- When the source catalog has no version, use the first year of adoption or the release date.
|
||||
- Make sure the version substring embedded in the filename matches `Version`, because the CLI dispatcher reads `compliance_framework.split("_")[1]` to select the correct version.
|
||||
- For **legacy** files, make sure the version substring embedded in the filename matches `Version`, because the CLI dispatcher reads `compliance_framework.split("_")[1]` to select the correct version.
|
||||
|
||||
## Validating the Framework Locally
|
||||
## Validating Your Framework
|
||||
|
||||
Follow the steps below before opening a pull request.
|
||||
Before opening a PR, validate the JSON loads cleanly against the model and that every referenced check actually exists.
|
||||
|
||||
### 1. Run the Compliance Model Validator
|
||||
### 1. Schema validation
|
||||
|
||||
For **universal** frameworks, load the file and inspect what was parsed. The framework key inside `bulk` is the **basename of the JSON file** (without `.json`); for `prowler/compliance/dora.json` that key is `dora`, for `prowler/compliance/aws/cis_5.0_aws.json` it is `cis_5.0_aws`.
|
||||
|
||||
```python
|
||||
from prowler.lib.check.compliance_models import (
|
||||
load_compliance_framework_universal,
|
||||
get_bulk_compliance_frameworks_universal,
|
||||
)
|
||||
|
||||
fw = load_compliance_framework_universal("prowler/compliance/<your_framework>.json")
|
||||
assert fw is not None, "load returned None — check the logs for the validation error"
|
||||
print(fw.framework, len(fw.requirements), fw.get_providers())
|
||||
|
||||
bulk = get_bulk_compliance_frameworks_universal("aws")
|
||||
assert "<your_framework_filename_without_json>" in bulk
|
||||
```
|
||||
|
||||
### 2. Check existence cross-check
|
||||
|
||||
There is **no automatic check-existence validation** at load time. Cross-check that every check name in your framework maps to a real check directory:
|
||||
|
||||
```python
|
||||
import os
|
||||
real = set()
|
||||
for svc in os.listdir("prowler/providers/aws/services"):
|
||||
svc_path = f"prowler/providers/aws/services/{svc}"
|
||||
if not os.path.isdir(svc_path):
|
||||
continue
|
||||
for entry in os.listdir(svc_path):
|
||||
if os.path.isfile(f"{svc_path}/{entry}/{entry}.metadata.json"):
|
||||
real.add(entry)
|
||||
|
||||
referenced = {c for r in fw.requirements for c in r.checks.get("aws", [])}
|
||||
missing = referenced - real
|
||||
assert not missing, f"checks referenced in framework but not found in repo: {sorted(missing)}"
|
||||
```
|
||||
|
||||
### 3. CLI smoke test
|
||||
|
||||
```bash
|
||||
uv run python prowler-cli.py <provider> --list-compliance
|
||||
```
|
||||
|
||||
The framework must appear in the output. A validation error indicates a schema mismatch between the JSON file and the attribute model.
|
||||
|
||||
### 2. Run a Scan Filtered by the Framework
|
||||
The framework must appear in the output. A validation error indicates a schema mismatch.
|
||||
|
||||
```bash
|
||||
uv run python prowler-cli.py <provider> \
|
||||
--compliance <framework>_<version>_<provider> \
|
||||
--compliance <framework_key> \
|
||||
--log-level ERROR
|
||||
```
|
||||
|
||||
Verify that:
|
||||
|
||||
- Prowler produces a CSV file under `output/compliance/` with the expected name.
|
||||
- The CLI summary table lists every section in the framework.
|
||||
- The CLI summary table lists every section / pillar of the framework.
|
||||
- Findings roll up under the expected requirements.
|
||||
|
||||
### 3. Inspect the CSV Output
|
||||
### 4. Inspect the CSV output
|
||||
|
||||
Open the generated CSV and confirm:
|
||||
|
||||
- All columns defined in `models.py` appear.
|
||||
- Every requirement has at least one row per scanned resource.
|
||||
- Values such as `Requirements_Attributes_Section` reflect the JSON content.
|
||||
- All columns defined in `models.py` (legacy) or in `attributes_metadata` (universal) appear.
|
||||
- Every requirement has at least one row per scanned resource (when there are findings).
|
||||
- Attribute values such as `Requirements_Attributes_Section` reflect the JSON content.
|
||||
|
||||
### 4. Verify the Framework in Prowler App
|
||||
### 5. Verify the framework in Prowler App
|
||||
|
||||
Launch Prowler App locally (`docker compose up` from the repository root) and run a scan with the new compliance framework. Confirm the compliance page renders the requirements, sections, and status widgets correctly.
|
||||
|
||||
@@ -331,7 +566,7 @@ Launch Prowler App locally (`docker compose up` from the repository root) and ru
|
||||
Compliance contributions require two layers of tests.
|
||||
|
||||
- **Schema tests** exercise the Pydantic models. Extend `tests/lib/check/universal_compliance_models_test.py` with a case that loads the new JSON file and asserts the attribute type matches the expected model.
|
||||
- **Output tests** exercise the transformer. Mirror the structure under `tests/lib/outputs/compliance/<framework>/` with fixtures that feed synthetic findings through the transformer and assert the resulting CSV rows.
|
||||
- **Output tests** (legacy frameworks only) exercise the transformer. Mirror the structure under `tests/lib/outputs/compliance/<framework>/` with fixtures that feed synthetic findings through the transformer and assert the resulting CSV rows.
|
||||
|
||||
Run the suite with:
|
||||
|
||||
@@ -342,7 +577,20 @@ uv run pytest -n auto tests/lib/check/universal_compliance_models_test.py \
|
||||
|
||||
For guidance on writing Prowler SDK tests, refer to [Unit Testing](/developer-guide/unit-testing).
|
||||
|
||||
## Submitting the Pull Request
|
||||
## Running and listing your framework
|
||||
|
||||
Once the file is in place, the CLI auto-discovers it:
|
||||
|
||||
```sh
|
||||
prowler <provider> --list-compliance # framework appears in the list
|
||||
prowler <provider> --compliance <framework_key> --list-checks
|
||||
prowler <provider> --compliance <framework_key> # full scan + compliance report
|
||||
prowler <provider> --compliance <framework_key> --list-compliance-requirements <framework_key>
|
||||
```
|
||||
|
||||
For end-user-facing tutorials (recommended for high-profile frameworks), add a dedicated page under `docs/user-guide/compliance/tutorials/` and register it in the `"Compliance"` group of `docs/docs.json`. See `docs/user-guide/compliance/tutorials/threatscore.mdx` as a reference.
|
||||
|
||||
## Submitting the pull request
|
||||
|
||||
Before opening the pull request:
|
||||
|
||||
@@ -352,28 +600,31 @@ Before opening the pull request:
|
||||
uv run pytest -n auto
|
||||
```
|
||||
2. Add a changelog entry under the `### 🚀 Added` section of `prowler/CHANGELOG.md`, describing the new framework and the providers it covers.
|
||||
3. Follow the [Pull Request Template](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md) and set the PR title using Conventional Commits, for example `feat(compliance): add My Framework 1.0 for AWS`.
|
||||
3. Follow the [Pull Request Template](https://github.com/prowler-cloud/prowler/blob/master/.github/pull_request_template.md) and set the PR title using Conventional Commits, e.g. `feat(compliance): add My Framework 1.0 for AWS`.
|
||||
4. Request review from the compliance codeowners listed in `.github/CODEOWNERS`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The following issues are the most common when contributing a compliance framework.
|
||||
|
||||
- **`ValidationError: field required` during scan.** The JSON is missing a required attribute field. Re-check the matching Pydantic model in `prowler/lib/check/compliance_models.py`.
|
||||
- **All attributes collapse to `Generic_Compliance_Requirement_Attribute` values.** The Pydantic `Union` is ordered incorrectly, or the JSON matches only the generic shape. Move the generic model to the last Union position and ensure every required field is present in the JSON.
|
||||
- **`--compliance` filter does not find the framework.** The filename does not match the expected pattern `<framework>_<version>_<provider>.json`, the version is empty, or the file lives outside `prowler/compliance/<provider>/`.
|
||||
- **CLI summary table is empty but the CSV is populated.** The dispatcher branch in `prowler/lib/outputs/compliance/compliance.py` is missing or its substring match does not catch the framework key.
|
||||
- **CSV file is missing after the scan.** The transformer class is not registered in `prowler/lib/outputs/compliance/compliance_output.py`, or `transform()` raises silently. Run the scan with `--log-level DEBUG`.
|
||||
- **Findings do not roll up under a requirement.** A check listed in `Checks` either does not exist for that provider or is spelled incorrectly. Run `--list-checks | grep <check_name>` to confirm.
|
||||
- **`ValidationError: field required` during scan (legacy).** The JSON is missing a required attribute field. Re-check the matching Pydantic model in `prowler/lib/check/compliance_models.py`.
|
||||
- **All attributes collapse to `Generic_Compliance_Requirement_Attribute` values (legacy).** The Pydantic `Union` is ordered incorrectly, or the JSON matches only the generic shape. Keep the generic model in the last Union position and ensure every required field is present in the JSON.
|
||||
- **`attributes_metadata validation failed` (universal).** The root validator in `compliance_models.py:669` rejected the file. The error message lists each offending requirement; common causes are unknown attribute keys (typo or missing entry in `attributes_metadata`), enum violations, or missing required keys.
|
||||
- **`--compliance` filter does not find the framework.** For legacy: the filename does not match `<framework>_<version>_<provider>.json`, the version is empty, or the file lives outside `prowler/compliance/<provider>/`. For universal: the file is not at the top level of `prowler/compliance/` or it loaded as `None` (check logs for the validation error).
|
||||
- **CLI summary table is empty but the CSV is populated (legacy).** The dispatcher branch in `prowler/lib/outputs/compliance/compliance.py` is missing or its substring match does not catch the framework key.
|
||||
- **CSV file is missing after the scan (legacy).** The transformer class is not registered in `prowler/lib/outputs/compliance/compliance_output.py`, or `transform()` raises silently. Run the scan with `--log-level DEBUG`.
|
||||
- **Findings do not roll up under a requirement.** A check listed in `Checks` either does not exist for that provider or is spelled incorrectly. Run `--list-checks | grep <check_name>` to confirm, or run the check-existence cross-check from "Validating Your Framework".
|
||||
|
||||
## Reference Examples
|
||||
## Reference examples
|
||||
|
||||
Use the following files as templates when modeling a new contribution.
|
||||
|
||||
- `prowler/compliance/aws/cis_2.0_aws.json` – CIS attribute shape.
|
||||
- `prowler/compliance/aws/nist_800_53_revision_5_aws.json` – Generic attribute shape.
|
||||
- `prowler/compliance/aws/ccc_aws.json` – CCC attribute shape.
|
||||
- `prowler/compliance/azure/ens_rd2022_azure.json` – ENS attribute shape.
|
||||
- `prowler/lib/check/compliance_models.py` – Canonical Pydantic schemas.
|
||||
- `prowler/lib/outputs/compliance/cis/` – Reference implementation of a multi-provider output formatter.
|
||||
- `prowler/lib/outputs/compliance/generic/` – Reference implementation of a generic output formatter.
|
||||
- `prowler/compliance/dora.json` — universal schema, single-provider populated (AWS), ready to extend with more providers.
|
||||
- `prowler/compliance/csa_ccm_4.0.json` — universal schema, multi-provider populated (AWS, Azure, GCP, AlibabaCloud, OracleCloud).
|
||||
- `prowler/compliance/aws/cis_2.0_aws.json` — legacy CIS attribute shape.
|
||||
- `prowler/compliance/aws/nist_800_53_revision_5_aws.json` — legacy generic attribute shape.
|
||||
- `prowler/compliance/aws/ccc_aws.json` — legacy CCC attribute shape.
|
||||
- `prowler/compliance/azure/ens_rd2022_azure.json` — legacy ENS attribute shape.
|
||||
- `prowler/lib/check/compliance_models.py` — canonical Pydantic schemas for both formats.
|
||||
- `prowler/lib/outputs/compliance/cis/` — reference implementation of a multi-provider legacy output formatter.
|
||||
- `prowler/lib/outputs/compliance/generic/` — reference implementation of a legacy generic output formatter.
|
||||
|
||||
@@ -7,6 +7,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
### 🚀 Added
|
||||
|
||||
- `sagemaker_models_monitor_enabled` check for AWS provider, verifying that each SageMaker monitoring schedule is in the `Scheduled` state so data and model drift is actively detected [(#11278)](https://github.com/prowler-cloud/prowler/pull/11278)
|
||||
- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) universal compliance framework with AWS provider coverage across the five DORA pillars [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -85,11 +85,6 @@ from prowler.lib.outputs.compliance.compliance import (
|
||||
display_compliance_table,
|
||||
process_universal_compliance_frameworks,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.csa.csa_alibabacloud import AlibabaCloudCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_aws import AWSCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_azure import AzureCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_gcp import GCPCSA
|
||||
from prowler.lib.outputs.compliance.csa.csa_oraclecloud import OracleCloudCSA
|
||||
from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS
|
||||
from prowler.lib.outputs.compliance.ens.ens_gcp import GCPENS
|
||||
@@ -806,18 +801,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(c5)
|
||||
c5.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_aws":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_aws = AWSCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_aws)
|
||||
csa_ccm_4_0_aws.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -921,18 +904,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(c5_azure)
|
||||
c5_azure.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_azure":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_azure = AzureCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_azure)
|
||||
csa_ccm_4_0_azure.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -1036,18 +1007,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(c5_gcp)
|
||||
c5_gcp.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_gcp":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_gcp = GCPCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_gcp)
|
||||
csa_ccm_4_0_gcp.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -1282,18 +1241,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
cis.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_oraclecloud":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_oraclecloud = OracleCloudCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_oraclecloud)
|
||||
csa_ccm_4_0_oraclecloud.batch_write_data_to_file()
|
||||
else:
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
@@ -1322,18 +1269,6 @@ def prowler():
|
||||
)
|
||||
generated_outputs["compliance"].append(cis)
|
||||
cis.batch_write_data_to_file()
|
||||
elif compliance_name == "csa_ccm_4.0_alibabacloud":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
f"{output_options.output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
csa_ccm_4_0_alibabacloud = AlibabaCloudCSA(
|
||||
findings=finding_outputs,
|
||||
compliance=bulk_compliance_frameworks[compliance_name],
|
||||
file_path=filename,
|
||||
)
|
||||
generated_outputs["compliance"].append(csa_ccm_4_0_alibabacloud)
|
||||
csa_ccm_4_0_alibabacloud.batch_write_data_to_file()
|
||||
elif compliance_name == "prowler_threatscore_alibabacloud":
|
||||
filename = (
|
||||
f"{output_options.output_directory}/compliance/"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,597 @@
|
||||
{
|
||||
"framework": "DORA",
|
||||
"name": "Digital Operational Resilience Act (Regulation (EU) 2022/2554)",
|
||||
"version": "2022/2554",
|
||||
"description": "The Digital Operational Resilience Act (DORA) is a European Union regulation (Regulation (EU) 2022/2554) that sets a uniform framework for the digital operational resilience of the EU financial sector. Mandatory since 17 January 2025, it applies to financial entities (banks, insurers, investment firms, payment institutions, etc.) and to ICT third-party service providers. DORA is structured around five pillars: ICT risk management, ICT-related incident reporting, digital operational resilience testing, ICT third-party risk management, and information sharing. This Prowler mapping covers the technical controls auditable from cloud configuration; the organisational, contractual and supervisory obligations defined in DORA must be addressed outside of Prowler.",
|
||||
"icon": "dora",
|
||||
"attributes_metadata": [
|
||||
{
|
||||
"key": "Pillar",
|
||||
"label": "Pillar",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"enum": [
|
||||
"ICT Risk Management",
|
||||
"ICT-Related Incident Reporting",
|
||||
"Digital Operational Resilience Testing",
|
||||
"ICT Third-Party Risk Management",
|
||||
"Information Sharing"
|
||||
],
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Article",
|
||||
"label": "Article",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "ArticleTitle",
|
||||
"label": "Article Title",
|
||||
"type": "str",
|
||||
"required": true,
|
||||
"output_formats": {
|
||||
"csv": true,
|
||||
"ocsf": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"table_config": {
|
||||
"group_by": "Pillar"
|
||||
},
|
||||
"pdf_config": {
|
||||
"language": "en",
|
||||
"primary_color": "#003399",
|
||||
"secondary_color": "#0055A5",
|
||||
"bg_color": "#F0F4FA",
|
||||
"group_by_field": "Pillar",
|
||||
"sections": [
|
||||
"ICT Risk Management",
|
||||
"ICT-Related Incident Reporting",
|
||||
"Digital Operational Resilience Testing",
|
||||
"ICT Third-Party Risk Management",
|
||||
"Information Sharing"
|
||||
],
|
||||
"section_short_names": {
|
||||
"ICT Risk Management": "ICT Risk Mgmt",
|
||||
"ICT-Related Incident Reporting": "Incident Reporting",
|
||||
"Digital Operational Resilience Testing": "Resilience Testing",
|
||||
"ICT Third-Party Risk Management": "Third-Party Risk",
|
||||
"Information Sharing": "Info Sharing"
|
||||
},
|
||||
"charts": [
|
||||
{
|
||||
"id": "pillar_compliance",
|
||||
"type": "horizontal_bar",
|
||||
"group_by": "Pillar",
|
||||
"title": "Compliance Score by DORA Pillar",
|
||||
"y_label": "Pillar",
|
||||
"x_label": "Compliance %",
|
||||
"value_source": "compliance_percent",
|
||||
"color_mode": "by_value"
|
||||
}
|
||||
],
|
||||
"filter": {
|
||||
"only_failed": true,
|
||||
"include_manual": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": [
|
||||
{
|
||||
"id": "DORA-Art5",
|
||||
"name": "Governance and organisation",
|
||||
"description": "Financial entities shall have a sound, comprehensive and well-documented ICT internal governance and control framework. Senior management is accountable for ICT risk and shall enforce strong identity, authentication and least-privilege policies for privileged identities, including the root account.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 5",
|
||||
"ArticleTitle": "Governance and organisation"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_avoid_root_usage",
|
||||
"iam_no_root_access_key",
|
||||
"iam_root_mfa_enabled",
|
||||
"iam_root_hardware_mfa_enabled",
|
||||
"iam_root_credentials_management_enabled",
|
||||
"iam_password_policy_minimum_length_14",
|
||||
"iam_password_policy_lowercase",
|
||||
"iam_password_policy_uppercase",
|
||||
"iam_password_policy_number",
|
||||
"iam_password_policy_symbol",
|
||||
"iam_password_policy_reuse_24",
|
||||
"iam_password_policy_expires_passwords_within_90_days_or_less",
|
||||
"iam_securityaudit_role_created",
|
||||
"iam_support_role_created",
|
||||
"organizations_account_part_of_organizations",
|
||||
"iam_user_mfa_enabled_console_access",
|
||||
"iam_user_hardware_mfa_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art6",
|
||||
"name": "ICT risk management framework",
|
||||
"description": "Financial entities shall have an ICT risk management framework that is sound, comprehensive and well-documented, enabling them to address ICT risk quickly, efficiently and comprehensively and to ensure a high level of digital operational resilience. This includes continuous configuration recording, security findings aggregation and an enterprise-wide visibility plane.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 6",
|
||||
"ArticleTitle": "ICT risk management framework"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"config_recorder_all_regions_enabled",
|
||||
"config_recorder_using_aws_service_role",
|
||||
"securityhub_enabled",
|
||||
"accessanalyzer_enabled",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"organizations_delegated_administrators",
|
||||
"guardduty_centrally_managed",
|
||||
"guardduty_delegated_admin_enabled_all_regions"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art7",
|
||||
"name": "ICT systems, protocols and tools",
|
||||
"description": "Financial entities shall use and maintain updated ICT systems, protocols and tools that are appropriate to the magnitude of operations supporting ICT functions, technologically resilient, and adequately equipped to securely process data. Cryptographic primitives, certificate hygiene and network segmentation are core to this requirement.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 7",
|
||||
"ArticleTitle": "ICT systems, protocols and tools"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"acm_certificates_with_secure_key_algorithms",
|
||||
"acm_certificates_transparency_logs_enabled",
|
||||
"acm_certificates_expiration_check",
|
||||
"ec2_ebs_default_encryption",
|
||||
"kms_cmk_rotation_enabled",
|
||||
"s3_bucket_secure_transport_policy",
|
||||
"s3_bucket_default_encryption",
|
||||
"s3_bucket_kms_encryption",
|
||||
"vpc_subnet_separate_private_public",
|
||||
"vpc_subnet_no_public_ip_by_default",
|
||||
"elb_insecure_ssl_ciphers",
|
||||
"elbv2_insecure_ssl_ciphers",
|
||||
"elb_ssl_listeners",
|
||||
"elbv2_ssl_listeners",
|
||||
"cloudfront_distributions_using_deprecated_ssl_protocols",
|
||||
"cloudfront_distributions_https_enabled",
|
||||
"rds_instance_transport_encrypted"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art8",
|
||||
"name": "Identification",
|
||||
"description": "Financial entities shall identify, classify and adequately document all ICT supported business functions, roles and responsibilities, the information assets and ICT assets supporting them, and their interdependencies. They shall on a continuous basis identify all sources of ICT risk, in particular the risk exposure to and from other financial entities.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 8",
|
||||
"ArticleTitle": "Identification"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"accessanalyzer_enabled",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"macie_is_enabled",
|
||||
"macie_automated_sensitive_data_discovery_enabled",
|
||||
"ec2_securitygroup_not_used",
|
||||
"ec2_elastic_ip_unassigned",
|
||||
"ec2_networkacl_unused",
|
||||
"secretsmanager_secret_unused"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art9",
|
||||
"name": "Protection and prevention",
|
||||
"description": "Financial entities shall continuously monitor and control the security and functioning of ICT systems and tools and minimise the impact of ICT risk by deploying appropriate ICT security tools, policies and procedures. Encryption at rest and in transit, blocking of public exposure, network access controls, secret management and instance hardening are central to this article.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 9",
|
||||
"ArticleTitle": "Protection and prevention"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"kms_key_not_publicly_accessible",
|
||||
"ec2_ebs_volume_encryption",
|
||||
"ec2_ebs_snapshots_encrypted",
|
||||
"ec2_ebs_public_snapshot",
|
||||
"ec2_ebs_snapshot_account_block_public_access",
|
||||
"s3_account_level_public_access_blocks",
|
||||
"s3_bucket_level_public_access_block",
|
||||
"s3_bucket_public_access",
|
||||
"s3_bucket_policy_public_write_access",
|
||||
"s3_bucket_public_write_acl",
|
||||
"s3_bucket_public_list_acl",
|
||||
"s3_bucket_acl_prohibited",
|
||||
"s3_access_point_public_access_block",
|
||||
"ec2_securitygroup_default_restrict_traffic",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_all_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_any_port",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22",
|
||||
"ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389",
|
||||
"rds_instance_storage_encrypted",
|
||||
"rds_cluster_storage_encrypted",
|
||||
"rds_instance_no_public_access",
|
||||
"rds_snapshots_public_access",
|
||||
"secretsmanager_not_publicly_accessible",
|
||||
"secretsmanager_has_restrictive_resource_policy",
|
||||
"secretsmanager_automatic_rotation_enabled",
|
||||
"dynamodb_tables_kms_cmk_encryption_enabled",
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"sns_topics_not_publicly_accessible",
|
||||
"ec2_instance_imdsv2_enabled",
|
||||
"ec2_instance_account_imdsv2_enabled",
|
||||
"efs_encryption_at_rest_enabled",
|
||||
"awslambda_function_not_publicly_accessible"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art10",
|
||||
"name": "Detection",
|
||||
"description": "Financial entities shall have in place mechanisms to promptly detect anomalous activities, including ICT network performance issues and ICT-related incidents, and to identify potential single points of failure. Threat detection across compute, identity, storage and the API control plane is required for timely detection.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 10",
|
||||
"ArticleTitle": "Detection"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"guardduty_ec2_malware_protection_enabled",
|
||||
"guardduty_lambda_protection_enabled",
|
||||
"guardduty_rds_protection_enabled",
|
||||
"guardduty_s3_protection_enabled",
|
||||
"guardduty_eks_audit_log_enabled",
|
||||
"guardduty_eks_runtime_monitoring_enabled",
|
||||
"securityhub_enabled",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_llm_jacking",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"cloudtrail_insights_exist",
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"ec2_elastic_ip_shodan"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art11",
|
||||
"name": "Response and recovery",
|
||||
"description": "Financial entities shall put in place a comprehensive ICT business continuity policy, including ICT response and recovery plans, that ensures the continuity of ICT-supported critical or important functions. Operational alarming, automated event routing and tested recovery actions are essential.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 11",
|
||||
"ArticleTitle": "Response and recovery"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"cloudwatch_alarm_actions_enabled",
|
||||
"cloudwatch_alarm_actions_alarm_state_configured",
|
||||
"eventbridge_global_endpoint_event_replication_enabled",
|
||||
"sns_subscription_not_using_http_endpoints",
|
||||
"backup_plans_exist",
|
||||
"backup_vaults_exist",
|
||||
"rds_instance_critical_event_subscription",
|
||||
"rds_cluster_critical_event_subscription"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art12",
|
||||
"name": "Backup policies and procedures, restoration and recovery procedures and methods",
|
||||
"description": "Financial entities shall develop and document backup policies and procedures specifying the scope of data subject to backup and the minimum frequency of the backup, as well as restoration and recovery procedures and methods. Backups must be encrypted, retained, and resources must be designed for recoverability across availability zones and regions.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 12",
|
||||
"ArticleTitle": "Backup policies and procedures, restoration and recovery procedures and methods"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"backup_plans_exist",
|
||||
"backup_vaults_exist",
|
||||
"backup_vaults_encrypted",
|
||||
"backup_recovery_point_encrypted",
|
||||
"backup_reportplans_exist",
|
||||
"rds_instance_backup_enabled",
|
||||
"rds_cluster_protected_by_backup_plan",
|
||||
"rds_instance_protected_by_backup_plan",
|
||||
"rds_instance_multi_az",
|
||||
"rds_cluster_multi_az",
|
||||
"rds_cluster_backtrack_enabled",
|
||||
"rds_instance_deletion_protection",
|
||||
"rds_cluster_deletion_protection",
|
||||
"rds_snapshots_encrypted",
|
||||
"s3_bucket_object_versioning",
|
||||
"s3_bucket_object_lock",
|
||||
"s3_bucket_cross_region_replication",
|
||||
"s3_bucket_no_mfa_delete",
|
||||
"dynamodb_tables_pitr_enabled",
|
||||
"dynamodb_table_deletion_protection_enabled",
|
||||
"ec2_ebs_volume_protected_by_backup_plan",
|
||||
"ec2_ebs_volume_snapshots_exists",
|
||||
"autoscaling_group_multiple_az",
|
||||
"elb_is_in_multiple_az",
|
||||
"elbv2_is_in_multiple_az",
|
||||
"cloudfront_distributions_multiple_origin_failover_configured",
|
||||
"dynamodb_table_protected_by_backup_plan"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art13",
|
||||
"name": "Learning and evolving",
|
||||
"description": "Financial entities shall have in place capabilities and staff to gather information on vulnerabilities and cyber threats, perform post ICT-related incident reviews, and continuously feed lessons learnt back into the ICT risk assessment process. Findings aggregation and continuous insights drive this cycle.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 13",
|
||||
"ArticleTitle": "Learning and evolving"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"securityhub_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"inspector2_active_findings_exist",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"cloudtrail_insights_exist"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art14",
|
||||
"name": "Communication",
|
||||
"description": "As part of the ICT risk management framework, financial entities shall have in place crisis communication plans enabling a responsible disclosure of ICT-related incidents or major vulnerabilities to clients, counterparts and the public. Reliable, encrypted and access-controlled notification channels are required.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Risk Management",
|
||||
"Article": "Article 14",
|
||||
"ArticleTitle": "Communication"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"sns_topics_kms_encryption_at_rest_enabled",
|
||||
"sns_topics_not_publicly_accessible",
|
||||
"sns_subscription_not_using_http_endpoints",
|
||||
"eventbridge_bus_exposed",
|
||||
"eventbridge_bus_cross_account_access",
|
||||
"eventbridge_schema_registry_cross_account_access",
|
||||
"cloudwatch_alarm_actions_enabled",
|
||||
"cloudwatch_alarm_actions_alarm_state_configured"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art17",
|
||||
"name": "ICT-related incident management process",
|
||||
"description": "Financial entities shall define, establish and implement an ICT-related incident management process to detect, manage and notify ICT-related incidents. Comprehensive trail logging, log integrity protection, retention and centralisation of ICT events are foundational requirements.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT-Related Incident Reporting",
|
||||
"Article": "Article 17",
|
||||
"ArticleTitle": "ICT-related incident management process"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"cloudtrail_multi_region_enabled",
|
||||
"cloudtrail_multi_region_enabled_logging_management_events",
|
||||
"cloudtrail_kms_encryption_enabled",
|
||||
"cloudtrail_log_file_validation_enabled",
|
||||
"cloudtrail_cloudwatch_logging_enabled",
|
||||
"cloudtrail_logs_s3_bucket_access_logging_enabled",
|
||||
"cloudtrail_logs_s3_bucket_is_not_publicly_accessible",
|
||||
"cloudtrail_s3_dataevents_read_enabled",
|
||||
"cloudtrail_s3_dataevents_write_enabled",
|
||||
"cloudtrail_bucket_requires_mfa_delete",
|
||||
"cloudtrail_bedrock_logging_enabled",
|
||||
"cloudwatch_log_group_retention_policy_specific_days_enabled",
|
||||
"cloudwatch_log_group_kms_encryption_enabled",
|
||||
"cloudwatch_log_group_no_secrets_in_logs",
|
||||
"cloudwatch_log_group_not_publicly_accessible",
|
||||
"vpc_flow_logs_enabled",
|
||||
"ec2_client_vpn_endpoint_connection_logging_enabled",
|
||||
"route53_public_hosted_zones_cloudwatch_logging_enabled",
|
||||
"elb_logging_enabled",
|
||||
"elbv2_logging_enabled",
|
||||
"cloudfront_distributions_logging_enabled",
|
||||
"s3_bucket_server_access_logging_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art18",
|
||||
"name": "Classification of ICT-related incidents and cyber threats",
|
||||
"description": "Financial entities shall classify ICT-related incidents and shall determine their impact based on criteria such as the number of clients affected, duration, geographical spread, data losses, and criticality of the services affected. Severity-aware threat detection across the estate underpins this classification.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT-Related Incident Reporting",
|
||||
"Article": "Article 18",
|
||||
"ArticleTitle": "Classification of ICT-related incidents and cyber threats"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"guardduty_no_high_severity_findings",
|
||||
"guardduty_centrally_managed",
|
||||
"guardduty_delegated_admin_enabled_all_regions",
|
||||
"securityhub_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_llm_jacking",
|
||||
"cloudtrail_threat_detection_privilege_escalation"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art19",
|
||||
"name": "Reporting of major ICT-related incidents and voluntary notification of significant cyber threats",
|
||||
"description": "Financial entities shall report major ICT-related incidents to the relevant competent authority and may, on a voluntary basis, notify significant cyber threats. Detective metric filters, change-tracking alarms and reliable notification topics are needed to surface and route reportable events.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT-Related Incident Reporting",
|
||||
"Article": "Article 19",
|
||||
"ArticleTitle": "Reporting of major ICT-related incidents and voluntary notification of significant cyber threats"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"cloudwatch_log_metric_filter_authentication_failures",
|
||||
"cloudwatch_log_metric_filter_unauthorized_api_calls",
|
||||
"cloudwatch_log_metric_filter_root_usage",
|
||||
"cloudwatch_log_metric_filter_sign_in_without_mfa",
|
||||
"cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk",
|
||||
"cloudwatch_log_metric_filter_for_s3_bucket_policy_changes",
|
||||
"cloudwatch_log_metric_filter_policy_changes",
|
||||
"cloudwatch_log_metric_filter_security_group_changes",
|
||||
"cloudwatch_log_metric_filter_aws_organizations_changes",
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled",
|
||||
"cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled",
|
||||
"cloudwatch_changes_to_network_acls_alarm_configured",
|
||||
"cloudwatch_changes_to_network_gateways_alarm_configured",
|
||||
"cloudwatch_changes_to_network_route_tables_alarm_configured",
|
||||
"cloudwatch_changes_to_vpcs_alarm_configured",
|
||||
"sns_subscription_not_using_http_endpoints"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art24",
|
||||
"name": "General requirements for the performance of digital operational resilience testing",
|
||||
"description": "Financial entities shall establish, maintain and review a sound and comprehensive digital operational resilience testing programme, as an integral part of the ICT risk management framework. Continuous vulnerability discovery, configuration assessment and instance manageability are foundational.",
|
||||
"attributes": {
|
||||
"Pillar": "Digital Operational Resilience Testing",
|
||||
"Article": "Article 24",
|
||||
"ArticleTitle": "General requirements for the performance of digital operational resilience testing"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"securityhub_enabled",
|
||||
"ec2_instance_managed_by_ssm",
|
||||
"ec2_instance_with_outdated_ami",
|
||||
"ssm_managed_compliant_patching"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art25",
|
||||
"name": "Testing of ICT tools and systems",
|
||||
"description": "Financial entities shall ensure that tests are undertaken on ICT tools and systems, on critical ICT systems supporting all critical or important functions, at least yearly. Vulnerability assessments, deprecated component detection and certificate hygiene must be tracked.",
|
||||
"attributes": {
|
||||
"Pillar": "Digital Operational Resilience Testing",
|
||||
"Article": "Article 25",
|
||||
"ArticleTitle": "Testing of ICT tools and systems"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"inspector2_is_enabled",
|
||||
"inspector2_active_findings_exist",
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_no_high_severity_findings",
|
||||
"config_recorder_all_regions_enabled",
|
||||
"ec2_instance_with_outdated_ami",
|
||||
"ec2_instance_managed_by_ssm",
|
||||
"ec2_instance_paravirtual_type",
|
||||
"rds_instance_deprecated_engine_version",
|
||||
"acm_certificates_expiration_check",
|
||||
"rds_instance_certificate_expiration",
|
||||
"iam_no_expired_server_certificates_stored",
|
||||
"ssm_managed_compliant_patching"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art28",
|
||||
"name": "General principles (ICT third-party risk)",
|
||||
"description": "Financial entities shall manage ICT third-party risk as an integral component of ICT risk within their ICT risk management framework. Cross-account access, trust boundaries, organization-level controls and dependency visibility are critical to monitor third-party exposure on AWS.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Third-Party Risk Management",
|
||||
"Article": "Article 28",
|
||||
"ArticleTitle": "General principles (ICT third-party risk)"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_role_cross_service_confused_deputy_prevention",
|
||||
"iam_role_cross_account_readonlyaccess_policy",
|
||||
"iam_no_custom_policy_permissive_role_assumption",
|
||||
"accessanalyzer_enabled",
|
||||
"accessanalyzer_enabled_without_findings",
|
||||
"s3_bucket_cross_account_access",
|
||||
"dynamodb_table_cross_account_access",
|
||||
"eventbridge_bus_cross_account_access",
|
||||
"eventbridge_schema_registry_cross_account_access",
|
||||
"cloudwatch_cross_account_sharing_disabled",
|
||||
"organizations_delegated_administrators",
|
||||
"organizations_account_part_of_organizations",
|
||||
"organizations_scp_check_deny_regions",
|
||||
"vpc_endpoint_connections_trust_boundaries",
|
||||
"vpc_endpoint_services_allowed_principals_trust_boundaries",
|
||||
"vpc_peering_routing_tables_with_least_privilege",
|
||||
"awslambda_function_using_cross_account_layers"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art30",
|
||||
"name": "Key contractual provisions",
|
||||
"description": "Contractual arrangements with ICT third-party service providers shall be set out in writing and include, at minimum, agreed service levels and clear allocation of rights and obligations. Privilege boundaries, least-privilege policies and absence of administrative wildcards are the technical guardrails that enforce these contractual constraints inside AWS.",
|
||||
"attributes": {
|
||||
"Pillar": "ICT Third-Party Risk Management",
|
||||
"Article": "Article 30",
|
||||
"ArticleTitle": "Key contractual provisions"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"iam_aws_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_attached_policy_no_administrative_privileges",
|
||||
"iam_customer_unattached_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_no_administrative_privileges",
|
||||
"iam_inline_policy_allows_privilege_escalation",
|
||||
"iam_policy_allows_privilege_escalation",
|
||||
"iam_inline_policy_no_full_access_to_cloudtrail",
|
||||
"iam_inline_policy_no_full_access_to_kms",
|
||||
"iam_policy_no_full_access_to_cloudtrail",
|
||||
"iam_policy_no_full_access_to_kms",
|
||||
"iam_role_administratoraccess_policy",
|
||||
"iam_user_administrator_access_policy",
|
||||
"iam_group_administrator_access_policy",
|
||||
"iam_administrator_access_with_mfa",
|
||||
"iam_policy_attached_only_to_group_or_roles",
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "DORA-Art45",
|
||||
"name": "Information-sharing arrangements on cyber threat information and intelligence",
|
||||
"description": "Financial entities may exchange amongst themselves cyber threat information and intelligence, including indicators of compromise, tactics, techniques and procedures, cyber security alerts and configuration tools. Centralised threat detection, sensitive data discovery and trail-based intelligence enable participation in such information-sharing arrangements.",
|
||||
"attributes": {
|
||||
"Pillar": "Information Sharing",
|
||||
"Article": "Article 45",
|
||||
"ArticleTitle": "Information-sharing arrangements on cyber threat information and intelligence"
|
||||
},
|
||||
"checks": {
|
||||
"aws": [
|
||||
"guardduty_is_enabled",
|
||||
"guardduty_centrally_managed",
|
||||
"securityhub_enabled",
|
||||
"macie_is_enabled",
|
||||
"macie_automated_sensitive_data_discovery_enabled",
|
||||
"cloudtrail_threat_detection_enumeration",
|
||||
"cloudtrail_threat_detection_llm_jacking",
|
||||
"cloudtrail_threat_detection_privilege_escalation",
|
||||
"accessanalyzer_enabled_without_findings"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ from prowler.lib.outputs.compliance.cis.cis import get_cis_table
|
||||
from prowler.lib.outputs.compliance.compliance_check import ( # noqa: F401 - re-export for backward compatibility
|
||||
get_check_compliance,
|
||||
)
|
||||
from prowler.lib.outputs.compliance.csa.csa import get_csa_table
|
||||
from prowler.lib.outputs.compliance.ens.ens import get_ens_table
|
||||
from prowler.lib.outputs.compliance.generic.generic_table import (
|
||||
get_generic_compliance_table,
|
||||
@@ -33,24 +32,28 @@ def process_universal_compliance_frameworks(
|
||||
output_filename: str,
|
||||
provider: str,
|
||||
generated_outputs: dict,
|
||||
from_cli: bool = True,
|
||||
is_last: bool = True,
|
||||
) -> set:
|
||||
"""Process universal compliance frameworks, generating CSV and OCSF outputs.
|
||||
|
||||
For each framework in *input_compliance_frameworks* that exists in
|
||||
*universal_frameworks* and has an outputs.table_config, this function
|
||||
creates both a CSV (UniversalComplianceOutput) and an OCSF JSON
|
||||
(OCSFComplianceOutput) file. OCSF is always generated regardless of
|
||||
*universal_frameworks* and has an ``outputs.table_config``, this function
|
||||
writes both a CSV (``UniversalComplianceOutput``) and an OCSF JSON
|
||||
(``OCSFComplianceOutput``) file. OCSF is always generated regardless of
|
||||
the user's ``--output-formats`` flag.
|
||||
|
||||
The function is idempotent: it tracks already-created writers via
|
||||
``generated_outputs["compliance"]`` keyed by ``file_path``. If invoked
|
||||
again for the same framework (e.g. once per streaming batch), it
|
||||
reuses the existing writer instead of recreating it. This guarantees
|
||||
one output writer per framework for the whole execution and keeps
|
||||
the OCSF JSON array valid across multiple calls.
|
||||
Streaming-aware: writers are tracked via ``generated_outputs["compliance"]``
|
||||
keyed by ``file_path``. On the first call per framework a new writer is
|
||||
created and emits both findings and manual requirements; subsequent calls
|
||||
reuse the writer, transform only the new ``finding_outputs`` (manual
|
||||
requirements are not re-emitted), and append to the open file. Set
|
||||
``from_cli=False`` and ``is_last=False`` for intermediate batches; pass
|
||||
``is_last=True`` on the final batch to close the file (OCSF is also
|
||||
finalized as a valid JSON array).
|
||||
|
||||
Returns the set of framework names that were processed so the caller
|
||||
can remove them before entering the legacy per-provider output loop.
|
||||
Returns the set of framework names processed so the caller can subtract
|
||||
them from the legacy per-provider output loop.
|
||||
"""
|
||||
from prowler.lib.outputs.compliance.universal.ocsf_compliance import (
|
||||
OCSFComplianceOutput,
|
||||
@@ -65,6 +68,13 @@ def process_universal_compliance_frameworks(
|
||||
if isinstance(out, (UniversalComplianceOutput, OCSFComplianceOutput))
|
||||
}
|
||||
|
||||
def _flush(writer, framework, label, is_new):
|
||||
if not is_new:
|
||||
writer._transform(finding_outputs, framework, label, include_manual=False)
|
||||
writer.close_file = is_last
|
||||
writer.batch_write_data_to_file()
|
||||
writer._data.clear()
|
||||
|
||||
processed = set()
|
||||
for compliance_name in input_compliance_frameworks:
|
||||
if not (
|
||||
@@ -75,37 +85,46 @@ def process_universal_compliance_frameworks(
|
||||
continue
|
||||
|
||||
fw = universal_frameworks[compliance_name]
|
||||
compliance_label = (
|
||||
fw.framework + "-" + fw.version if fw.version else fw.framework
|
||||
)
|
||||
|
||||
# CSV output
|
||||
csv_path = (
|
||||
f"{output_directory}/compliance/" f"{output_filename}_{compliance_name}.csv"
|
||||
)
|
||||
if csv_path not in existing_writers:
|
||||
output = UniversalComplianceOutput(
|
||||
csv_writer = existing_writers.get(csv_path)
|
||||
csv_is_new = csv_writer is None
|
||||
if csv_is_new:
|
||||
csv_writer = UniversalComplianceOutput(
|
||||
findings=finding_outputs,
|
||||
framework=fw,
|
||||
file_path=csv_path,
|
||||
from_cli=from_cli,
|
||||
provider=provider,
|
||||
)
|
||||
generated_outputs["compliance"].append(output)
|
||||
existing_writers[csv_path] = output
|
||||
output.batch_write_data_to_file()
|
||||
generated_outputs["compliance"].append(csv_writer)
|
||||
existing_writers[csv_path] = csv_writer
|
||||
_flush(csv_writer, fw, compliance_label, csv_is_new)
|
||||
|
||||
# OCSF output (always generated for universal frameworks)
|
||||
ocsf_path = (
|
||||
f"{output_directory}/compliance/"
|
||||
f"{output_filename}_{compliance_name}.ocsf.json"
|
||||
)
|
||||
if ocsf_path not in existing_writers:
|
||||
ocsf_output = OCSFComplianceOutput(
|
||||
ocsf_writer = existing_writers.get(ocsf_path)
|
||||
ocsf_is_new = ocsf_writer is None
|
||||
if ocsf_is_new:
|
||||
ocsf_writer = OCSFComplianceOutput(
|
||||
findings=finding_outputs,
|
||||
framework=fw,
|
||||
file_path=ocsf_path,
|
||||
from_cli=from_cli,
|
||||
provider=provider,
|
||||
)
|
||||
generated_outputs["compliance"].append(ocsf_output)
|
||||
existing_writers[ocsf_path] = ocsf_output
|
||||
ocsf_output.batch_write_data_to_file()
|
||||
generated_outputs["compliance"].append(ocsf_writer)
|
||||
existing_writers[ocsf_path] = ocsf_writer
|
||||
_flush(ocsf_writer, fw, compliance_label, ocsf_is_new)
|
||||
|
||||
processed.add(compliance_name)
|
||||
|
||||
@@ -206,15 +225,6 @@ def display_compliance_table(
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
elif compliance_framework.startswith("csa_ccm_"):
|
||||
get_csa_table(
|
||||
findings,
|
||||
bulk_checks_metadata,
|
||||
compliance_framework,
|
||||
output_filename,
|
||||
output_directory,
|
||||
compliance_overview,
|
||||
)
|
||||
elif compliance_framework.startswith("c5_"):
|
||||
get_c5_table(
|
||||
findings,
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
from colorama import Fore, Style
|
||||
from tabulate import tabulate
|
||||
|
||||
from prowler.config.config import orange_color
|
||||
|
||||
|
||||
def get_csa_table(
|
||||
findings: list,
|
||||
bulk_checks_metadata: dict,
|
||||
compliance_framework: str,
|
||||
output_filename: str,
|
||||
output_directory: str,
|
||||
compliance_overview: bool,
|
||||
):
|
||||
section_table = {
|
||||
"Provider": [],
|
||||
"Section": [],
|
||||
"Status": [],
|
||||
"Muted": [],
|
||||
}
|
||||
pass_count = []
|
||||
fail_count = []
|
||||
muted_count = []
|
||||
sections = {}
|
||||
for index, finding in enumerate(findings):
|
||||
check = bulk_checks_metadata[finding.check_metadata.CheckID]
|
||||
check_compliances = check.Compliance
|
||||
for compliance in check_compliances:
|
||||
if (
|
||||
compliance.Framework == "CSA-CCM"
|
||||
and compliance.Version in compliance_framework
|
||||
):
|
||||
for requirement in compliance.Requirements:
|
||||
for attribute in requirement.Attributes:
|
||||
section = attribute.Section
|
||||
|
||||
if section not in sections:
|
||||
sections[section] = {"FAIL": 0, "PASS": 0, "Muted": 0}
|
||||
|
||||
if finding.muted:
|
||||
if index not in muted_count:
|
||||
muted_count.append(index)
|
||||
sections[section]["Muted"] += 1
|
||||
else:
|
||||
if finding.status == "FAIL" and index not in fail_count:
|
||||
fail_count.append(index)
|
||||
sections[section]["FAIL"] += 1
|
||||
elif finding.status == "PASS" and index not in pass_count:
|
||||
pass_count.append(index)
|
||||
sections[section]["PASS"] += 1
|
||||
|
||||
sections = dict(sorted(sections.items()))
|
||||
for section in sections:
|
||||
section_table["Provider"].append(compliance.Provider)
|
||||
section_table["Section"].append(section)
|
||||
if sections[section]["FAIL"] > 0:
|
||||
section_table["Status"].append(
|
||||
f"{Fore.RED}FAIL({sections[section]['FAIL']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
if sections[section]["PASS"] > 0:
|
||||
section_table["Status"].append(
|
||||
f"{Fore.GREEN}PASS({sections[section]['PASS']}){Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
section_table["Status"].append(f"{Fore.GREEN}PASS{Style.RESET_ALL}")
|
||||
section_table["Muted"].append(
|
||||
f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
if (
|
||||
len(fail_count) + len(pass_count) + len(muted_count) > 1
|
||||
): # If there are no resources, don't print the compliance table
|
||||
print(
|
||||
f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:"
|
||||
)
|
||||
total_findings_count = len(fail_count) + len(pass_count) + len(muted_count)
|
||||
overview_table = [
|
||||
[
|
||||
f"{Fore.RED}{round(len(fail_count) / total_findings_count * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}",
|
||||
f"{Fore.GREEN}{round(len(pass_count) / total_findings_count * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}",
|
||||
f"{orange_color}{round(len(muted_count) / total_findings_count * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}",
|
||||
]
|
||||
]
|
||||
print(tabulate(overview_table, tablefmt="rounded_grid"))
|
||||
if not compliance_overview:
|
||||
if len(fail_count) > 0 and len(section_table["Section"]) > 0:
|
||||
print(
|
||||
f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:"
|
||||
)
|
||||
print(
|
||||
tabulate(
|
||||
section_table,
|
||||
tablefmt="rounded_grid",
|
||||
headers="keys",
|
||||
)
|
||||
)
|
||||
print(f"\nDetailed results of {compliance_framework.upper()} are in:")
|
||||
print(
|
||||
f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n"
|
||||
)
|
||||
@@ -1,95 +0,0 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import AlibabaCloudCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AlibabaCloudCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the Alibaba Cloud CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into Alibaba Cloud CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into Alibaba Cloud CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
for requirement in compliance.Requirements:
|
||||
# Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift).
|
||||
if finding.check_id in requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AlibabaCloudCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=finding.account_uid,
|
||||
Region=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AlibabaCloudCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
AccountId="",
|
||||
Region="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -1,95 +0,0 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import AWSCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AWSCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the AWS CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into AWS CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into AWS CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
for requirement in compliance.Requirements:
|
||||
# Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift).
|
||||
if finding.check_id in requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AWSCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
AccountId=finding.account_uid,
|
||||
Region=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AWSCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
AccountId="",
|
||||
Region="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -1,95 +0,0 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import AzureCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class AzureCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the Azure CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into Azure CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into Azure CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
for requirement in compliance.Requirements:
|
||||
# Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift).
|
||||
if finding.check_id in requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AzureCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
SubscriptionId=finding.account_uid,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = AzureCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
SubscriptionId="",
|
||||
Location="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -1,95 +0,0 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import GCPCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class GCPCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the GCP CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into GCP CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into GCP CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
for requirement in compliance.Requirements:
|
||||
# Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift).
|
||||
if finding.check_id in requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = GCPCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
ProjectId=finding.account_uid,
|
||||
Location=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = GCPCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
ProjectId="",
|
||||
Location="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -1,95 +0,0 @@
|
||||
from prowler.config.config import timestamp
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput
|
||||
from prowler.lib.outputs.compliance.csa.models import OracleCloudCSAModel
|
||||
from prowler.lib.outputs.finding import Finding
|
||||
|
||||
|
||||
class OracleCloudCSA(ComplianceOutput):
|
||||
"""
|
||||
This class represents the OracleCloud CSA compliance output.
|
||||
|
||||
Attributes:
|
||||
- _data (list): A list to store transformed data from findings.
|
||||
- _file_descriptor (TextIOWrapper): A file descriptor to write data to a file.
|
||||
|
||||
Methods:
|
||||
- transform: Transforms findings into OracleCloud CSA compliance format.
|
||||
"""
|
||||
|
||||
def transform(
|
||||
self,
|
||||
findings: list[Finding],
|
||||
compliance: Compliance,
|
||||
compliance_name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Transforms a list of findings into OracleCloud CSA compliance format.
|
||||
|
||||
Parameters:
|
||||
- findings (list): A list of findings.
|
||||
- compliance (Compliance): A compliance model.
|
||||
- compliance_name (str): The name of the compliance model.
|
||||
|
||||
Returns:
|
||||
- None
|
||||
"""
|
||||
for finding in findings:
|
||||
for requirement in compliance.Requirements:
|
||||
# Source of truth: framework JSON, not finding.compliance snapshot (avoids CSV/UI count drift).
|
||||
if finding.check_id in requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = OracleCloudCSAModel(
|
||||
Provider=finding.provider,
|
||||
Description=compliance.Description,
|
||||
TenancyId=finding.account_uid,
|
||||
Region=finding.region,
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status=finding.status,
|
||||
StatusExtended=finding.status_extended,
|
||||
ResourceId=finding.resource_uid,
|
||||
ResourceName=finding.resource_name,
|
||||
CheckId=finding.check_id,
|
||||
Muted=finding.muted,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
# Add manual requirements to the compliance output
|
||||
for requirement in compliance.Requirements:
|
||||
if not requirement.Checks:
|
||||
for attribute in requirement.Attributes:
|
||||
compliance_row = OracleCloudCSAModel(
|
||||
Provider=compliance.Provider.lower(),
|
||||
Description=compliance.Description,
|
||||
TenancyId="",
|
||||
Region="",
|
||||
AssessmentDate=str(timestamp),
|
||||
Requirements_Id=requirement.Id,
|
||||
Requirements_Description=requirement.Description,
|
||||
Requirements_Name=requirement.Name,
|
||||
Requirements_Attributes_Section=attribute.Section,
|
||||
Requirements_Attributes_CCMLite=attribute.CCMLite,
|
||||
Requirements_Attributes_IaaS=attribute.IaaS,
|
||||
Requirements_Attributes_PaaS=attribute.PaaS,
|
||||
Requirements_Attributes_SaaS=attribute.SaaS,
|
||||
Requirements_Attributes_ScopeApplicability=attribute.ScopeApplicability,
|
||||
Status="MANUAL",
|
||||
StatusExtended="Manual check",
|
||||
ResourceId="manual_check",
|
||||
ResourceName="Manual check",
|
||||
CheckId="manual",
|
||||
Muted=False,
|
||||
Framework=compliance.Framework,
|
||||
Name=compliance.Name,
|
||||
)
|
||||
self._data.append(compliance_row)
|
||||
@@ -1,146 +0,0 @@
|
||||
from pydantic.v1 import BaseModel
|
||||
|
||||
|
||||
class AWSCSAModel(BaseModel):
|
||||
"""
|
||||
AWSCSAModel generates a finding's output in CSV CSA format for AWS.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
AccountId: str
|
||||
Region: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class GCPCSAModel(BaseModel):
|
||||
"""
|
||||
GCPCSAModel generates a finding's output in CSV CSA format for GCP.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
ProjectId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class OracleCloudCSAModel(BaseModel):
|
||||
"""
|
||||
OracleCloudCSAModel generates a finding's output in CSV CSA format for OracleCloud.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
TenancyId: str
|
||||
Region: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class AlibabaCloudCSAModel(BaseModel):
|
||||
"""
|
||||
AlibabaCloudCSAModel generates a finding's output in CSV CSA format for Alibaba Cloud.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
AccountId: str
|
||||
Region: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
|
||||
|
||||
class AzureCSAModel(BaseModel):
|
||||
"""
|
||||
AzureCSAModel generates a finding's output in CSV CSA format for Azure.
|
||||
"""
|
||||
|
||||
Provider: str
|
||||
Description: str
|
||||
SubscriptionId: str
|
||||
Location: str
|
||||
AssessmentDate: str
|
||||
Requirements_Id: str
|
||||
Requirements_Description: str
|
||||
Requirements_Name: str
|
||||
Requirements_Attributes_Section: str
|
||||
Requirements_Attributes_CCMLite: str
|
||||
Requirements_Attributes_IaaS: str
|
||||
Requirements_Attributes_PaaS: str
|
||||
Requirements_Attributes_SaaS: str
|
||||
Requirements_Attributes_ScopeApplicability: list[dict]
|
||||
Status: str
|
||||
StatusExtended: str
|
||||
ResourceId: str
|
||||
CheckId: str
|
||||
Muted: bool
|
||||
ResourceName: str
|
||||
Framework: str
|
||||
Name: str
|
||||
@@ -79,30 +79,43 @@ def _to_snake_case(name: str) -> str:
|
||||
return s.lower()
|
||||
|
||||
|
||||
def _build_requirement_attrs(requirement, framework) -> dict:
|
||||
"""Build a dict with requirement attributes for the unmapped section.
|
||||
def _build_requirement_attrs(requirement, framework):
|
||||
"""Build the requirement attributes payload for the unmapped section.
|
||||
|
||||
Keys are normalized to snake_case for OCSF consistency.
|
||||
Only includes attributes whose AttributeMetadata has output_formats.ocsf=True.
|
||||
When no metadata is declared, all attributes are included.
|
||||
Keys are snake_cased and filtered by ``AttributeMetadata.output_formats.ocsf``
|
||||
when declared. MITRE-style attrs (``{"_raw_attributes": [...]}``) are
|
||||
unwrapped into a list of per-entry dicts.
|
||||
"""
|
||||
attrs = requirement.attributes
|
||||
if not attrs:
|
||||
requirement_attributes = requirement.attributes
|
||||
if not requirement_attributes:
|
||||
return {}
|
||||
|
||||
# Build set of keys allowed for OCSF output
|
||||
metadata = framework.attributes_metadata
|
||||
if metadata:
|
||||
ocsf_keys = {m.key for m in metadata if m.output_formats.ocsf}
|
||||
else:
|
||||
ocsf_keys = None # No metadata → include all
|
||||
allowed_keys = (
|
||||
{entry.key for entry in metadata if entry.output_formats.ocsf}
|
||||
if metadata
|
||||
else None
|
||||
)
|
||||
|
||||
result = {}
|
||||
for key, value in attrs.items():
|
||||
if ocsf_keys is not None and key not in ocsf_keys:
|
||||
continue
|
||||
result[_to_snake_case(key)] = value
|
||||
return result
|
||||
def _to_snake_case_dict(entry: dict) -> dict:
|
||||
return {
|
||||
_to_snake_case(key): value
|
||||
for key, value in entry.items()
|
||||
if allowed_keys is None or key in allowed_keys
|
||||
}
|
||||
|
||||
if (
|
||||
isinstance(requirement_attributes, dict)
|
||||
and "_raw_attributes" in requirement_attributes
|
||||
):
|
||||
raw_entries = requirement_attributes.get("_raw_attributes") or []
|
||||
return [
|
||||
_to_snake_case_dict(entry)
|
||||
for entry in raw_entries
|
||||
if isinstance(entry, dict)
|
||||
]
|
||||
|
||||
return _to_snake_case_dict(requirement_attributes)
|
||||
|
||||
|
||||
class OCSFComplianceOutput:
|
||||
@@ -147,7 +160,14 @@ class OCSFComplianceOutput:
|
||||
findings: List["Finding"],
|
||||
framework: ComplianceFramework,
|
||||
compliance_name: str,
|
||||
include_manual: bool = True,
|
||||
) -> None:
|
||||
"""Transform findings into OCSF ComplianceFinding events.
|
||||
|
||||
Manual requirements are emitted only when ``include_manual=True``. The
|
||||
caller must pass ``False`` for subsequent streaming batches so manual
|
||||
events are not duplicated.
|
||||
"""
|
||||
# Build check -> requirements map
|
||||
check_req_map = {}
|
||||
for req in framework.requirements:
|
||||
@@ -170,6 +190,9 @@ class OCSFComplianceOutput:
|
||||
if cf:
|
||||
self._data.append(cf)
|
||||
|
||||
if not include_manual:
|
||||
return
|
||||
|
||||
# Manual requirements (no checks or empty for current provider)
|
||||
for req in framework.requirements:
|
||||
checks = req.checks
|
||||
|
||||
@@ -198,8 +198,15 @@ class UniversalComplianceOutput:
|
||||
findings: list["Finding"],
|
||||
framework: ComplianceFramework,
|
||||
compliance_name: str,
|
||||
include_manual: bool = True,
|
||||
) -> None:
|
||||
"""Transform findings into universal compliance CSV rows."""
|
||||
"""Transform findings into universal compliance CSV rows.
|
||||
|
||||
Manual requirements (no checks or empty for current provider) are
|
||||
emitted only when ``include_manual=True``. When the writer is reused
|
||||
across streaming batches, the caller should pass ``False`` after the
|
||||
first batch so manual rows are not duplicated.
|
||||
"""
|
||||
# Build check -> requirements map (filtered by provider for dict checks)
|
||||
check_req_map = {}
|
||||
for req in framework.requirements:
|
||||
@@ -228,6 +235,9 @@ class UniversalComplianceOutput:
|
||||
except Exception as e:
|
||||
logger.debug(f"Skipping row for {req.id}: {e}")
|
||||
|
||||
if not include_manual:
|
||||
return
|
||||
|
||||
# Manual requirements (no checks or empty dict)
|
||||
for req in framework.requirements:
|
||||
checks = req.checks
|
||||
|
||||
@@ -94,21 +94,6 @@ class TestDispatchStartswith:
|
||||
display_compliance_table(compliance_framework=framework_name, **_COMMON)
|
||||
mock_fn.assert_called_once()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"framework_name",
|
||||
[
|
||||
"csa_ccm_4.0_aws",
|
||||
"csa_ccm_4.0_azure",
|
||||
"csa_ccm_4.0_gcp",
|
||||
"csa_ccm_4.0_oraclecloud",
|
||||
"csa_ccm_4.0_alibabacloud",
|
||||
],
|
||||
)
|
||||
@patch(f"{MODULE}.get_csa_table")
|
||||
def test_csa_dispatch(self, mock_fn, framework_name):
|
||||
display_compliance_table(compliance_framework=framework_name, **_COMMON)
|
||||
mock_fn.assert_called_once()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"framework_name",
|
||||
["c5_aws", "c5_azure", "c5_gcp"],
|
||||
|
||||
@@ -12,6 +12,7 @@ Also validates that print_compliance_frameworks and print_compliance_requirement
|
||||
work with universal ComplianceFramework objects (dict checks, None provider).
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
@@ -124,6 +125,41 @@ def _make_universal_framework(name="TestFW", version="1.0", with_table_config=Tr
|
||||
)
|
||||
|
||||
|
||||
def _make_framework_with_manual(name="MixedFW", version="1.0"):
|
||||
"""Framework with one aws-covered requirement and one manual one.
|
||||
|
||||
The manual requirement has no aws checks, so for provider ``aws`` it is
|
||||
emitted as a manual row/event — used to assert manual requirements are
|
||||
not duplicated when the writer is reused across streaming batches.
|
||||
"""
|
||||
reqs = [
|
||||
UniversalComplianceRequirement(
|
||||
id="1.1",
|
||||
description="Covered requirement",
|
||||
attributes={"Section": "IAM"},
|
||||
checks={"aws": ["check_a"]},
|
||||
),
|
||||
UniversalComplianceRequirement(
|
||||
id="2.1",
|
||||
description="Manual requirement",
|
||||
attributes={"Section": "GOV"},
|
||||
checks={"aws": []},
|
||||
),
|
||||
]
|
||||
metadata = [AttributeMetadata(key="Section", type="str")]
|
||||
outputs = OutputsConfig(table_config=TableConfig(group_by="Section"))
|
||||
return ComplianceFramework(
|
||||
framework=name,
|
||||
name=f"{name} Framework",
|
||||
provider="AWS",
|
||||
version=version,
|
||||
description="Test framework",
|
||||
requirements=reqs,
|
||||
attributes_metadata=metadata,
|
||||
outputs=outputs,
|
||||
)
|
||||
|
||||
|
||||
# ── Tests ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -728,3 +764,243 @@ class TestIdempotency:
|
||||
# FW1 writer instances unchanged
|
||||
assert second_writers[0] is first_writers[0]
|
||||
assert second_writers[1] is first_writers[1]
|
||||
|
||||
|
||||
class TestStreamingBatches:
|
||||
"""Streaming-aware behaviour: ``from_cli`` / ``is_last`` / ``_flush``.
|
||||
|
||||
Regression coverage for the API streaming path where the helper is
|
||||
invoked once per finding batch: before the fix only the first batch
|
||||
was written (batches 2..N silently dropped) and manual requirements
|
||||
were re-emitted on every batch.
|
||||
"""
|
||||
|
||||
def _run_batches(self, tmp_path, fw, key, batches):
|
||||
"""Invoke the helper once per (findings, is_last) batch, sharing
|
||||
``generated_outputs`` so writers are reused like the API does."""
|
||||
generated = {"compliance": []}
|
||||
for findings, is_last in batches:
|
||||
process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks={key},
|
||||
universal_frameworks={key: fw},
|
||||
finding_outputs=findings,
|
||||
output_directory=str(tmp_path),
|
||||
output_filename="out",
|
||||
provider="aws",
|
||||
generated_outputs=generated,
|
||||
from_cli=False,
|
||||
is_last=is_last,
|
||||
)
|
||||
return generated
|
||||
|
||||
def test_defaults_preserve_cli_single_call(self, tmp_path):
|
||||
"""Defaults (``from_cli=True``, ``is_last=True``): a single call
|
||||
still finalizes a valid, closed OCSF JSON array (CLI unchanged)."""
|
||||
fw = _make_universal_framework()
|
||||
generated = {"compliance": []}
|
||||
process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks={"test_fw_1.0"},
|
||||
universal_frameworks={"test_fw_1.0": fw},
|
||||
finding_outputs=[_make_finding("check_a")],
|
||||
output_directory=str(tmp_path),
|
||||
output_filename="out",
|
||||
provider="aws",
|
||||
generated_outputs=generated,
|
||||
)
|
||||
ocsf_path = tmp_path / "compliance" / "out_test_fw_1.0.ocsf.json"
|
||||
data = json.loads(ocsf_path.read_text())
|
||||
assert isinstance(data, list) and len(data) >= 1
|
||||
|
||||
def test_multibatch_csv_keeps_every_batch(self, tmp_path):
|
||||
"""Findings from batches 2..N must not be dropped (the bug)."""
|
||||
fw = _make_universal_framework()
|
||||
f1 = _make_finding("check_a", status="PASS")
|
||||
f2 = _make_finding("check_a", status="FAIL")
|
||||
generated = self._run_batches(
|
||||
tmp_path, fw, "fw_1.0", [([f1], False), ([f2], True)]
|
||||
)
|
||||
content = (tmp_path / "compliance" / "out_fw_1.0.csv").read_text()
|
||||
assert "check_a is PASS" in content # batch 1
|
||||
assert "check_a is FAIL" in content # batch 2 — regression
|
||||
# writer reused, not recreated: still just 1 CSV + 1 OCSF
|
||||
assert len(generated["compliance"]) == 2
|
||||
|
||||
def test_multibatch_ocsf_valid_array_with_every_batch(self, tmp_path):
|
||||
"""OCSF is a valid (closed) JSON array holding every batch's
|
||||
events only after the ``is_last=True`` call."""
|
||||
fw = _make_universal_framework()
|
||||
f1 = _make_finding("check_a", status="PASS")
|
||||
f2 = _make_finding("check_a", status="FAIL")
|
||||
self._run_batches(tmp_path, fw, "fw_1.0", [([f1], False), ([f2], True)])
|
||||
data = json.loads(
|
||||
(tmp_path / "compliance" / "out_fw_1.0.ocsf.json").read_text()
|
||||
)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 2 # one event per batch finding
|
||||
|
||||
def test_manual_requirement_not_duplicated_across_batches(self, tmp_path):
|
||||
"""Manual requirement is emitted once (first batch, via __init__),
|
||||
never re-emitted when the writer is reused (``include_manual=False``)."""
|
||||
fw = _make_framework_with_manual()
|
||||
f1 = _make_finding("check_a", status="PASS")
|
||||
f2 = _make_finding("check_a", status="FAIL")
|
||||
self._run_batches(tmp_path, fw, "fw_1.0", [([f1], False), ([f2], True)])
|
||||
rows = list(
|
||||
csv.DictReader(
|
||||
(tmp_path / "compliance" / "out_fw_1.0.csv").read_text().splitlines(),
|
||||
delimiter=";",
|
||||
)
|
||||
)
|
||||
manual_rows = [r for r in rows if r["STATUS"] == "MANUAL"]
|
||||
assert len(manual_rows) == 1
|
||||
assert manual_rows[0]["REQUIREMENTS_ID"] == "2.1"
|
||||
|
||||
ocsf = json.loads(
|
||||
(tmp_path / "compliance" / "out_fw_1.0.ocsf.json").read_text()
|
||||
)
|
||||
manual_events = [
|
||||
e
|
||||
for e in ocsf
|
||||
if (e.get("compliance") or {}).get("requirements") == ["2.1"]
|
||||
]
|
||||
assert len(manual_events) == 1
|
||||
|
||||
def test_writer_reused_not_recreated_across_batches(self, tmp_path):
|
||||
"""Three batches still yield exactly one CSV + one OCSF writer,
|
||||
and the same instances are reused throughout."""
|
||||
fw = _make_universal_framework()
|
||||
generated = self._run_batches(
|
||||
tmp_path,
|
||||
fw,
|
||||
"fw_1.0",
|
||||
[
|
||||
([_make_finding("check_a")], False),
|
||||
([_make_finding("check_a")], False),
|
||||
([_make_finding("check_a")], True),
|
||||
],
|
||||
)
|
||||
assert len(generated["compliance"]) == 2
|
||||
assert isinstance(generated["compliance"][0], UniversalComplianceOutput)
|
||||
assert isinstance(generated["compliance"][1], OCSFComplianceOutput)
|
||||
|
||||
def test_label_without_version_still_outputs(self, tmp_path):
|
||||
"""Empty framework version → label is the framework name only;
|
||||
the helper still produces both artifacts without error."""
|
||||
fw = _make_universal_framework(version="")
|
||||
generated = {"compliance": []}
|
||||
processed = process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks={"fw"},
|
||||
universal_frameworks={"fw": fw},
|
||||
finding_outputs=[_make_finding("check_a")],
|
||||
output_directory=str(tmp_path),
|
||||
output_filename="out",
|
||||
provider="aws",
|
||||
generated_outputs=generated,
|
||||
from_cli=False,
|
||||
is_last=True,
|
||||
)
|
||||
assert processed == {"fw"}
|
||||
assert len(generated["compliance"]) == 2
|
||||
assert (tmp_path / "compliance" / "out_fw.csv").exists()
|
||||
assert (tmp_path / "compliance" / "out_fw.ocsf.json").exists()
|
||||
|
||||
|
||||
def _csa_like_framework() -> ComplianceFramework:
|
||||
"""Build a CSA CCM-style universal framework with checks across providers."""
|
||||
requirement = UniversalComplianceRequirement(
|
||||
id="A&A-01",
|
||||
description="Audit and Assurance",
|
||||
attributes={"Section": "Audit"},
|
||||
checks={
|
||||
"aws": ["aws_check"],
|
||||
"azure": ["azure_check"],
|
||||
"gcp": ["gcp_check"],
|
||||
},
|
||||
)
|
||||
return ComplianceFramework(
|
||||
framework="CSA_CCM",
|
||||
name="CSA Cloud Controls Matrix",
|
||||
version="4.0",
|
||||
description="Multi-provider framework",
|
||||
requirements=[requirement],
|
||||
attributes_metadata=[AttributeMetadata(key="Section", type="str")],
|
||||
outputs=OutputsConfig(table_config=TableConfig(group_by="Section")),
|
||||
)
|
||||
|
||||
|
||||
class TestMultiProviderUniversalFramework:
|
||||
"""A top-level CSA-CCM-style framework produces a CSV+OCSF pair scoped
|
||||
to the provider it is invoked with."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider,check_id",
|
||||
[
|
||||
("aws", "aws_check"),
|
||||
("azure", "azure_check"),
|
||||
("gcp", "gcp_check"),
|
||||
],
|
||||
)
|
||||
def test_per_provider_outputs_isolated(self, tmp_path, provider, check_id):
|
||||
framework = _csa_like_framework()
|
||||
generated = {"compliance": []}
|
||||
|
||||
process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks={"csa_ccm_4.0"},
|
||||
universal_frameworks={"csa_ccm_4.0": framework},
|
||||
finding_outputs=[_make_finding(check_id, provider=provider)],
|
||||
output_directory=str(tmp_path),
|
||||
output_filename="prowler_output",
|
||||
provider=provider,
|
||||
generated_outputs=generated,
|
||||
)
|
||||
|
||||
ocsf_path = tmp_path / "compliance" / "prowler_output_csa_ccm_4.0.ocsf.json"
|
||||
events = json.loads(ocsf_path.read_text())
|
||||
assert isinstance(events, list)
|
||||
non_manual = [event for event in events if event.get("status_code") != "MANUAL"]
|
||||
assert len(non_manual) == 1
|
||||
assert non_manual[0]["compliance"]["checks"][0]["uid"] == check_id
|
||||
|
||||
|
||||
class TestMitreStyleOCSFOutput:
|
||||
"""MITRE attrs wrapped as `{"_raw_attributes": [...]}` must not leak
|
||||
the marker key through the OCSF pipeline."""
|
||||
|
||||
def test_mitre_raw_attributes_pass_through_pipeline(self, tmp_path):
|
||||
mitre_requirement = UniversalComplianceRequirement(
|
||||
id="T1078",
|
||||
description="Valid Accounts",
|
||||
attributes={
|
||||
"_raw_attributes": [{"AWSService": "IAM", "Category": "Initial Access"}]
|
||||
},
|
||||
checks={"aws": ["check_a"]},
|
||||
)
|
||||
framework = ComplianceFramework(
|
||||
framework="MITRE",
|
||||
name="MITRE ATT&CK",
|
||||
version="14",
|
||||
description="Mitre",
|
||||
requirements=[mitre_requirement],
|
||||
outputs=OutputsConfig(table_config=TableConfig(group_by="AWSService")),
|
||||
)
|
||||
generated = {"compliance": []}
|
||||
|
||||
process_universal_compliance_frameworks(
|
||||
input_compliance_frameworks={"mitre_attack_aws"},
|
||||
universal_frameworks={"mitre_attack_aws": framework},
|
||||
finding_outputs=[_make_finding("check_a", "PASS")],
|
||||
output_directory=str(tmp_path),
|
||||
output_filename="out",
|
||||
provider="aws",
|
||||
generated_outputs=generated,
|
||||
)
|
||||
|
||||
ocsf_path = tmp_path / "compliance" / "out_mitre_attack_aws.ocsf.json"
|
||||
events = json.loads(ocsf_path.read_text())
|
||||
assert isinstance(events, list) and len(events) >= 1
|
||||
for event in events:
|
||||
requirement_attrs = (event.get("unmapped") or {}).get(
|
||||
"requirement_attributes", {}
|
||||
)
|
||||
assert "_raw_attributes" not in requirement_attrs
|
||||
assert "raw_attributes" not in requirement_attrs
|
||||
|
||||
@@ -202,6 +202,26 @@ class TestOCSFComplianceOutput:
|
||||
assert cf.status_code == "MANUAL"
|
||||
assert cf.finding_info.uid == "manual-MANUAL-1"
|
||||
|
||||
def test_include_manual_false_skips_manual(self):
|
||||
"""``_transform(..., include_manual=False)`` emits check events but
|
||||
NOT manual requirement events. The streaming caller passes ``False``
|
||||
for batches 2..N so manual events are not duplicated."""
|
||||
covered = _simple_requirement("REQ-1", ["check_a"])
|
||||
manual = _simple_requirement("MANUAL-1", checks=[])
|
||||
fw = _make_framework([covered, manual])
|
||||
findings = [_make_finding("check_a")]
|
||||
|
||||
output = OCSFComplianceOutput(findings=findings, framework=fw, provider="aws")
|
||||
# __init__ transforms with include_manual=True (default) → manual present
|
||||
assert any(cf.status_code == "MANUAL" for cf in output.data)
|
||||
|
||||
# A subsequent batch re-transforms with include_manual=False
|
||||
output._data.clear()
|
||||
output._transform(findings, fw, "TestFW-1.0", include_manual=False)
|
||||
|
||||
assert len(output.data) == 1 # only the check event, no manual
|
||||
assert all(cf.status_code != "MANUAL" for cf in output.data)
|
||||
|
||||
def test_multi_provider_checks_dict(self):
|
||||
req = UniversalComplianceRequirement(
|
||||
id="REQ-1",
|
||||
@@ -631,3 +651,103 @@ class TestNoTopLevelOCSFImport:
|
||||
import prowler.lib.outputs.compliance.universal.ocsf_compliance as mod
|
||||
|
||||
assert "OCSF" not in dir(mod)
|
||||
|
||||
|
||||
def _mitre_requirement(req_id="T1078", entries=None):
|
||||
"""Build a MITRE-style requirement with `_raw_attributes` wrapping."""
|
||||
return UniversalComplianceRequirement(
|
||||
id=req_id,
|
||||
description="Valid Accounts",
|
||||
attributes={
|
||||
"_raw_attributes": entries
|
||||
or [{"AWSService": "IAM", "Category": "Initial Access"}]
|
||||
},
|
||||
checks={"aws": ["check_a"]},
|
||||
)
|
||||
|
||||
|
||||
class TestMitreRawAttributes:
|
||||
"""MITRE attrs wrapped as `{"_raw_attributes": [...]}` must not leak
|
||||
the marker key into the OCSF payload."""
|
||||
|
||||
def test_raw_attributes_key_not_in_unmapped(self):
|
||||
framework = _make_framework([_mitre_requirement()])
|
||||
findings = [_make_finding("check_a", "PASS")]
|
||||
|
||||
output = OCSFComplianceOutput(
|
||||
findings=findings, framework=framework, provider="aws"
|
||||
)
|
||||
|
||||
requirement_attrs = (output.data[0].unmapped or {}).get(
|
||||
"requirement_attributes", {}
|
||||
)
|
||||
assert "_raw_attributes" not in requirement_attrs
|
||||
assert "raw_attributes" not in requirement_attrs
|
||||
|
||||
def test_finding_serializes_with_raw_attributes(self):
|
||||
framework = _make_framework(
|
||||
[
|
||||
_mitre_requirement(
|
||||
entries=[
|
||||
{"AWSService": "IAM", "Category": "Initial Access"},
|
||||
{"AWSService": "STS", "Category": "Privilege Escalation"},
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
findings = [_make_finding("check_a", "PASS")]
|
||||
|
||||
output = OCSFComplianceOutput(
|
||||
findings=findings, framework=framework, provider="aws"
|
||||
)
|
||||
compliance_finding = output.data[0]
|
||||
if hasattr(compliance_finding, "model_dump_json"):
|
||||
payload = json.loads(compliance_finding.model_dump_json(exclude_none=True))
|
||||
else:
|
||||
payload = json.loads(compliance_finding.json(exclude_none=True))
|
||||
assert payload["compliance"]["requirements"] == ["T1078"]
|
||||
|
||||
|
||||
class TestProviderFiltering:
|
||||
"""OCSF writer scopes findings against `requirement.checks[provider]`."""
|
||||
|
||||
def test_check_for_other_provider_not_emitted(self):
|
||||
azure_only_requirement = UniversalComplianceRequirement(
|
||||
id="REQ-1",
|
||||
description="Azure-only requirement",
|
||||
attributes={},
|
||||
checks={"azure": ["check_a"]},
|
||||
)
|
||||
framework = _make_framework([azure_only_requirement])
|
||||
findings = [_make_finding("check_a", "PASS", provider="aws")]
|
||||
|
||||
output = OCSFComplianceOutput(
|
||||
findings=findings, framework=framework, provider="aws"
|
||||
)
|
||||
|
||||
assert all(
|
||||
compliance_finding.status_code == "MANUAL"
|
||||
for compliance_finding in output.data
|
||||
)
|
||||
|
||||
def test_no_provider_aggregates_all_checks(self):
|
||||
multi_provider_requirement = UniversalComplianceRequirement(
|
||||
id="REQ-1",
|
||||
description="Multi-provider requirement",
|
||||
attributes={},
|
||||
checks={"aws": ["check_a"], "azure": ["check_b"]},
|
||||
)
|
||||
framework = _make_framework([multi_provider_requirement])
|
||||
findings = [
|
||||
_make_finding("check_a", "PASS", provider="aws"),
|
||||
_make_finding("check_b", "FAIL", provider="azure"),
|
||||
]
|
||||
|
||||
output = OCSFComplianceOutput(
|
||||
findings=findings, framework=framework, provider=None
|
||||
)
|
||||
|
||||
statuses = sorted(
|
||||
compliance_finding.status_code for compliance_finding in output.data
|
||||
)
|
||||
assert statuses == ["FAIL", "PASS"]
|
||||
|
||||
@@ -122,6 +122,43 @@ class TestManualRequirements:
|
||||
assert manual_rows[0].dict()["Requirements_Id"] == "manual-1"
|
||||
assert manual_rows[0].dict()["ResourceId"] == "manual_check"
|
||||
|
||||
def test_include_manual_false_skips_manual_rows(self, tmp_path):
|
||||
"""``_transform(..., include_manual=False)`` emits finding rows but
|
||||
NOT manual requirements. The streaming caller passes ``False`` for
|
||||
batches 2..N so manual rows are not duplicated across batches."""
|
||||
reqs = [
|
||||
UniversalComplianceRequirement(
|
||||
id="1.1",
|
||||
description="test",
|
||||
attributes={"Section": "IAM"},
|
||||
checks={"aws": ["check_a"]},
|
||||
),
|
||||
UniversalComplianceRequirement(
|
||||
id="manual-1",
|
||||
description="manual check",
|
||||
attributes={"Section": "Governance"},
|
||||
checks={},
|
||||
),
|
||||
]
|
||||
metadata = [AttributeMetadata(key="Section", type="str")]
|
||||
fw = _make_framework(reqs, metadata, TableConfig(group_by="Section"))
|
||||
findings = [_make_finding("check_a", "PASS", {"TestFW-1.0": ["1.1"]})]
|
||||
|
||||
output = UniversalComplianceOutput(
|
||||
findings=findings,
|
||||
framework=fw,
|
||||
file_path=str(tmp_path / "t.csv"),
|
||||
)
|
||||
# __init__ transforms with include_manual=True (default) → manual present
|
||||
assert any(r.dict()["Status"] == "MANUAL" for r in output.data)
|
||||
|
||||
# A subsequent batch re-transforms with include_manual=False
|
||||
output._data.clear()
|
||||
output._transform(findings, fw, "TestFW-1.0", include_manual=False)
|
||||
|
||||
assert len(output.data) == 1 # only the finding row, no manual
|
||||
assert all(r.dict()["Status"] != "MANUAL" for r in output.data)
|
||||
|
||||
|
||||
class TestMITREExtraColumns:
|
||||
def test_mitre_columns_present(self, tmp_path):
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.30.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- DORA compliance framework support [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131)
|
||||
|
||||
---
|
||||
|
||||
## [1.29.0] (Prowler v5.29.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -394,6 +394,27 @@ export const getComplianceCsv = async (scanId: string, complianceId: string) =>
|
||||
"compliance report",
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the OCSF JSON export for a universal compliance framework.
|
||||
*
|
||||
* Only universal frameworks that declare an ``outputs`` block (today: DORA,
|
||||
* CSA CCM 4.0) produce a per-framework OCSF artifact. For any other framework
|
||||
* the backend returns 404; callers should gate this download via
|
||||
* ``isOcsfSupported(framework)``.
|
||||
*
|
||||
* NOTE: this is a dedicated path (``compliance/{id}/ocsf``), not a query
|
||||
* param. The API's JSON:API ``QueryParameterValidationFilter`` rejects any
|
||||
* non-JSON:API query param with 400, so ``?type=`` / ``?format=`` is not an
|
||||
* option — the format must be encoded in the route.
|
||||
*/
|
||||
export const getComplianceOcsf = async (scanId: string, complianceId: string) =>
|
||||
_fetchScanBinary(
|
||||
scanId,
|
||||
`compliance/${complianceId}/ocsf`,
|
||||
`scan-${scanId}-compliance-${complianceId}.ocsf.json`,
|
||||
"compliance OCSF report",
|
||||
);
|
||||
|
||||
/**
|
||||
* Get a compliance PDF report for any supported framework.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
interface DORADetailsProps {
|
||||
requirement: Requirement;
|
||||
}
|
||||
|
||||
export const DORACustomDetails = ({ requirement }: DORADetailsProps) => {
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.pillar && (
|
||||
<ComplianceBadge
|
||||
label="Pillar"
|
||||
value={requirement.pillar as string}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
{requirement.article && (
|
||||
<ComplianceBadge
|
||||
label="Article"
|
||||
value={requirement.article as string}
|
||||
color="indigo"
|
||||
/>
|
||||
)}
|
||||
{requirement.article_title && (
|
||||
<ComplianceBadge
|
||||
label="Article Title"
|
||||
value={requirement.article_title as string}
|
||||
color="gray"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -6,15 +6,19 @@ import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { downloadComplianceCsvMock, downloadCompliancePdfMock } = vi.hoisted(
|
||||
() => ({
|
||||
downloadComplianceCsvMock: vi.fn(),
|
||||
downloadCompliancePdfMock: vi.fn(),
|
||||
}),
|
||||
);
|
||||
const {
|
||||
downloadComplianceCsvMock,
|
||||
downloadComplianceOcsfMock,
|
||||
downloadCompliancePdfMock,
|
||||
} = vi.hoisted(() => ({
|
||||
downloadComplianceCsvMock: vi.fn(),
|
||||
downloadComplianceOcsfMock: vi.fn(),
|
||||
downloadCompliancePdfMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/helper", () => ({
|
||||
downloadComplianceCsv: downloadComplianceCsvMock,
|
||||
downloadComplianceOcsf: downloadComplianceOcsfMock,
|
||||
downloadCompliancePdf: downloadCompliancePdfMock,
|
||||
}));
|
||||
|
||||
@@ -131,4 +135,51 @@ describe("ComplianceDownloadContainer", () => {
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide the OCSF action for frameworks without OCSF support", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ComplianceDownloadContainer
|
||||
compact
|
||||
presentation="dropdown"
|
||||
scanId="scan-1"
|
||||
complianceId="compliance-1"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Open compliance export actions" }),
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Download OCSF report")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should surface and trigger the OCSF download for universal frameworks", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ComplianceDownloadContainer
|
||||
compact
|
||||
presentation="dropdown"
|
||||
scanId="scan-1"
|
||||
complianceId="dora"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Open compliance export actions" }),
|
||||
);
|
||||
expect(screen.getByText("Download OCSF report")).toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("menuitem", { name: /Download OCSF report/i }),
|
||||
);
|
||||
|
||||
expect(downloadComplianceOcsfMock).toHaveBeenCalledWith(
|
||||
"scan-1",
|
||||
"dora",
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { DownloadIcon, FileTextIcon } from "lucide-react";
|
||||
import { DownloadIcon, FileJsonIcon, FileTextIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
@@ -14,8 +14,15 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { toast } from "@/components/ui";
|
||||
import type { ComplianceReportType } from "@/lib/compliance/compliance-report-types";
|
||||
import { downloadComplianceCsv, downloadCompliancePdf } from "@/lib/helper";
|
||||
import {
|
||||
type ComplianceReportType,
|
||||
isOcsfSupported,
|
||||
} from "@/lib/compliance/compliance-report-types";
|
||||
import {
|
||||
downloadComplianceCsv,
|
||||
downloadComplianceOcsf,
|
||||
downloadCompliancePdf,
|
||||
} from "@/lib/helper";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ComplianceDownloadContainerProps {
|
||||
@@ -40,9 +47,14 @@ export const ComplianceDownloadContainer = ({
|
||||
presentation = "buttons",
|
||||
}: ComplianceDownloadContainerProps) => {
|
||||
const [isDownloadingCsv, setIsDownloadingCsv] = useState(false);
|
||||
const [isDownloadingOcsf, setIsDownloadingOcsf] = useState(false);
|
||||
const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
|
||||
const isIconWidth = buttonWidth === "icon";
|
||||
const isDropdown = presentation === "dropdown";
|
||||
// Only universal frameworks declaring an ``outputs`` block expose a
|
||||
// per-framework OCSF artifact (today: DORA, CSA CCM 4.0). Hide the
|
||||
// action everywhere else so the user never hits a guaranteed 404.
|
||||
const ocsfAvailable = isOcsfSupported(complianceId);
|
||||
|
||||
const handleDownloadCsv = async () => {
|
||||
if (isDownloadingCsv) return;
|
||||
@@ -54,6 +66,16 @@ export const ComplianceDownloadContainer = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadOcsf = async () => {
|
||||
if (!ocsfAvailable || isDownloadingOcsf) return;
|
||||
setIsDownloadingOcsf(true);
|
||||
try {
|
||||
await downloadComplianceOcsf(scanId, complianceId, toast);
|
||||
} finally {
|
||||
setIsDownloadingOcsf(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadPdf = async () => {
|
||||
if (!reportType || isDownloadingPdf) return;
|
||||
setIsDownloadingPdf(true);
|
||||
@@ -105,6 +127,18 @@ export const ComplianceDownloadContainer = ({
|
||||
onSelect={handleDownloadCsv}
|
||||
disabled={disabled || isDownloadingCsv}
|
||||
/>
|
||||
{ocsfAvailable && (
|
||||
<ActionDropdownItem
|
||||
icon={
|
||||
<FileJsonIcon
|
||||
className={isDownloadingOcsf ? "animate-download-icon" : ""}
|
||||
/>
|
||||
}
|
||||
label="Download OCSF report"
|
||||
onSelect={handleDownloadOcsf}
|
||||
disabled={disabled || isDownloadingOcsf}
|
||||
/>
|
||||
)}
|
||||
{reportType && (
|
||||
<ActionDropdownItem
|
||||
icon={
|
||||
@@ -152,6 +186,29 @@ export const ComplianceDownloadContainer = ({
|
||||
<TooltipContent>Download CSV report</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
{ocsfAvailable && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
onClick={handleDownloadOcsf}
|
||||
disabled={disabled || isDownloadingOcsf}
|
||||
aria-label="Download compliance OCSF report"
|
||||
>
|
||||
<FileJsonIcon
|
||||
size={14}
|
||||
className={isDownloadingOcsf ? "animate-download-icon" : ""}
|
||||
/>
|
||||
<span className={labelClassName}>OCSF</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{showTooltip && (
|
||||
<TooltipContent>Download OCSF report</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{reportType && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -6,6 +6,7 @@ import CCCLogo from "./ccc.svg";
|
||||
import CISLogo from "./cis.svg";
|
||||
import CISALogo from "./cisa.svg";
|
||||
import CSALogo from "./csa.svg";
|
||||
import DORALogo from "./dora.svg";
|
||||
import ENSLogo from "./ens.png";
|
||||
import FedRAMPLogo from "./fedramp.svg";
|
||||
import FFIECLogo from "./ffiec.svg";
|
||||
@@ -67,6 +68,9 @@ const COMPLIANCE_LOGOS = [
|
||||
["c5", C5Logo],
|
||||
["ccc", CCCLogo],
|
||||
["csa", CSALogo],
|
||||
// DORA — universal framework (`prowler/compliance/dora.json`). The
|
||||
// compliance_id is just `dora`, no provider suffix.
|
||||
["dora", DORALogo],
|
||||
["secnumcloud", ANSSILogo],
|
||||
["aws", AWSLogo],
|
||||
] as const;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 170" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="doraGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#003399"/>
|
||||
<stop offset="100%" style="stop-color:#0055A5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<rect x="0" y="20" width="400" height="130" rx="16" fill="url(#doraGradient)"/>
|
||||
<text x="200" y="100" font-family="Helvetica, Arial, sans-serif" font-size="76" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="6">DORA</text>
|
||||
<text x="200" y="135" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="500" fill="#FFD700" text-anchor="middle" letter-spacing="3">EU 2022/2554</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 747 B |
@@ -6,6 +6,7 @@ import { C5CustomDetails } from "@/components/compliance/compliance-custom-detai
|
||||
import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details";
|
||||
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
|
||||
import { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details";
|
||||
import { DORACustomDetails } from "@/components/compliance/compliance-custom-details/dora-details";
|
||||
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
|
||||
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
|
||||
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
|
||||
@@ -47,6 +48,10 @@ import {
|
||||
mapComplianceData as mapCSAComplianceData,
|
||||
toAccordionItems as toCSAAccordionItems,
|
||||
} from "./csa";
|
||||
import {
|
||||
mapComplianceData as mapDORAComplianceData,
|
||||
toAccordionItems as toDORAAccordionItems,
|
||||
} from "./dora";
|
||||
import {
|
||||
mapComplianceData as mapENSComplianceData,
|
||||
toAccordionItems as toENSAccordionItems,
|
||||
@@ -208,6 +213,19 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(CSACustomDetails, { requirement }),
|
||||
},
|
||||
// DORA (Regulation (EU) 2022/2554) — universal framework keyed by the
|
||||
// `framework` field of `prowler/compliance/dora.json` ("DORA"). Groups by
|
||||
// Pillar (5 enum values) and surfaces Pillar / Article / ArticleTitle in
|
||||
// the requirement detail drawer.
|
||||
DORA: {
|
||||
mapComplianceData: mapDORAComplianceData,
|
||||
toAccordionItems: toDORAAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(DORACustomDetails, { requirement }),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
COMPLIANCE_REPORT_TYPES,
|
||||
getReportTypeForCompliance,
|
||||
getReportTypeForFramework,
|
||||
isOcsfSupported,
|
||||
pickLatestCisPerProvider,
|
||||
} from "./compliance-report-types";
|
||||
|
||||
@@ -34,6 +35,24 @@ describe("getReportTypeForFramework", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isOcsfSupported", () => {
|
||||
it("returns true for universal frameworks shipping an OCSF artifact", () => {
|
||||
expect(isOcsfSupported("dora")).toBe(true);
|
||||
expect(isOcsfSupported("csa_ccm_4.0")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for legacy/per-provider frameworks without OCSF output", () => {
|
||||
expect(isOcsfSupported("cis_5.0_aws")).toBe(false);
|
||||
expect(isOcsfSupported("ens_rd2022_aws")).toBe(false);
|
||||
expect(isOcsfSupported("nis2_aws")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for missing or empty inputs", () => {
|
||||
expect(isOcsfSupported(undefined)).toBe(false);
|
||||
expect(isOcsfSupported("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickLatestCisPerProvider", () => {
|
||||
it("returns an empty set for an empty input", () => {
|
||||
const latest = pickLatestCisPerProvider([]);
|
||||
@@ -95,7 +114,7 @@ describe("pickLatestCisPerProvider", () => {
|
||||
const latest = pickLatestCisPerProvider([
|
||||
"ens_rd2022_aws",
|
||||
"nis2_aws",
|
||||
"csa_ccm_4.0_aws",
|
||||
"csa_ccm_4.0",
|
||||
"prowler_threatscore_aws",
|
||||
"cis_5.0_aws",
|
||||
]);
|
||||
|
||||
@@ -161,6 +161,30 @@ export const pickLatestCisPerProvider = (
|
||||
return latest;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compliance IDs that ship a per-framework OCSF JSON export.
|
||||
*
|
||||
* Only universal compliance frameworks that declare an ``outputs`` block in
|
||||
* their schema (see ``prowler/compliance/<name>.json``) produce a dedicated
|
||||
* OCSF artifact during scan output generation. Today that is DORA and
|
||||
* CSA CCM 4.0. Any other framework only offers CSV (and, for the curated
|
||||
* list above, PDF).
|
||||
*
|
||||
* Keep this Set in lock-step with the backend: ``get_prowler_provider_compliance``
|
||||
* + ``ComplianceFramework.outputs`` is the source of truth. The API will
|
||||
* 404 on ``GET /scans/{id}/compliance/{name}/ocsf`` for any framework not
|
||||
* in this set, so showing the OCSF button for an unsupported framework
|
||||
* would surface a broken download — gate every call site through
|
||||
* ``isOcsfSupported``.
|
||||
*/
|
||||
const OCSF_SUPPORTED_COMPLIANCE_IDS: ReadonlySet<string> = new Set([
|
||||
"dora",
|
||||
"csa_ccm_4.0",
|
||||
]);
|
||||
|
||||
export const isOcsfSupported = (complianceId: string | undefined): boolean =>
|
||||
!!complianceId && OCSF_SUPPORTED_COMPLIANCE_IDS.has(complianceId);
|
||||
|
||||
/**
|
||||
* Resolve the report type for a compliance card.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
DORAAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
calculateFrameworkCounters,
|
||||
createRequirementsMap,
|
||||
findOrCreateCategory,
|
||||
findOrCreateControl,
|
||||
findOrCreateFramework,
|
||||
} from "./commons";
|
||||
|
||||
// Display order for DORA pillars in the accordion and any grouped chart. The
|
||||
// regulation arranges them in this exact order (Articles 5-14, 17-19, 24-25,
|
||||
// 28+30, 45) — preserving it here means the UI always renders pillars in the
|
||||
// "logical" reading order regardless of how the API returns them.
|
||||
export const DORA_PILLAR_ORDER: readonly string[] = [
|
||||
"ICT Risk Management",
|
||||
"ICT-Related Incident Reporting",
|
||||
"Digital Operational Resilience Testing",
|
||||
"ICT Third-Party Risk Management",
|
||||
"Information Sharing",
|
||||
];
|
||||
|
||||
const getStatusCounters = (status: RequirementStatus) => ({
|
||||
pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
});
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirementsMap = createRequirementsMap(requirementsData);
|
||||
const frameworks: Framework[] = [];
|
||||
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes
|
||||
?.metadata as unknown as DORAAttributesMetadata[];
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!attrs) continue;
|
||||
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
// Group by Pillar (top-level accordion section). Article + ArticleTitle
|
||||
// live inside the requirement so they show up on the detail drawer.
|
||||
const categoryName = attrs.Pillar;
|
||||
const requirementName = attributeItem.attributes.name || "";
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
const category = findOrCreateCategory(framework.categories, categoryName);
|
||||
// Flat 2-level structure: pillar → requirements (no intermediate control).
|
||||
const control = findOrCreateControl(category.controls, categoryName);
|
||||
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
const requirement: Requirement = {
|
||||
name: requirementName ? `${id} - ${requirementName}` : id,
|
||||
description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
...getStatusCounters(finalStatus),
|
||||
pillar: attrs.Pillar,
|
||||
article: attrs.Article,
|
||||
article_title: attrs.ArticleTitle,
|
||||
};
|
||||
|
||||
control.requirements.push(requirement);
|
||||
}
|
||||
|
||||
// Sort categories by canonical pillar order so DORA always reads from "ICT
|
||||
// Risk Management" down to "Information Sharing", regardless of map insertion
|
||||
// order driven by the API response.
|
||||
for (const framework of frameworks) {
|
||||
framework.categories.sort((a, b) => {
|
||||
const ia = DORA_PILLAR_ORDER.indexOf(a.name);
|
||||
const ib = DORA_PILLAR_ORDER.indexOf(b.name);
|
||||
// Unknown pillars (defensive — shouldn't happen) sink to the bottom.
|
||||
const orderA = ia === -1 ? DORA_PILLAR_ORDER.length : ia;
|
||||
const orderB = ib === -1 ? DORA_PILLAR_ORDER.length : ib;
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
|
||||
calculateFrameworkCounters(frameworks);
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
const safeId = scanId || "";
|
||||
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => ({
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
// Pillar → requirements (flat, no intermediate "control" level).
|
||||
items: category.controls.flatMap((control) =>
|
||||
control.requirements.map((requirement, reqIndex) => ({
|
||||
key: `${framework.name}-${category.name}-req-${reqIndex}`,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${framework.name}-${category.name}-req-${reqIndex}`}
|
||||
requirement={requirement}
|
||||
scanId={safeId}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
})),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
getComplianceCsv,
|
||||
getComplianceOcsf,
|
||||
getCompliancePdfReport,
|
||||
type ScanBinaryResult,
|
||||
} from "@/actions/scans";
|
||||
@@ -247,6 +248,32 @@ export const downloadComplianceCsv = async (
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Download the per-framework OCSF JSON export.
|
||||
*
|
||||
* Only universal frameworks declaring an ``outputs`` block produce this
|
||||
* artifact (currently DORA and CSA CCM 4.0); callers must gate the call
|
||||
* via ``isOcsfSupported`` to avoid surfacing a broken download on
|
||||
* frameworks the API will 404 on.
|
||||
*/
|
||||
export const downloadComplianceOcsf = async (
|
||||
scanId: string,
|
||||
complianceId: string,
|
||||
toast: ReturnType<typeof useToast>["toast"],
|
||||
): Promise<void> => {
|
||||
toast({
|
||||
title: "Download Started",
|
||||
description: "Preparing the OCSF report. This may take a moment.",
|
||||
});
|
||||
const result = await getComplianceOcsf(scanId, complianceId);
|
||||
await downloadFile(
|
||||
result,
|
||||
"application/json",
|
||||
"The compliance OCSF report has been downloaded successfully.",
|
||||
toast,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Download a compliance PDF report.
|
||||
*
|
||||
|
||||
@@ -327,6 +327,31 @@ export interface ASDEssentialEightRequirement extends Requirement {
|
||||
references: ASDEssentialEightAttributesMetadata["References"];
|
||||
}
|
||||
|
||||
// DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554).
|
||||
// Universal framework — flat attributes dict with Pillar/Article/ArticleTitle.
|
||||
// `Pillar` is the canonical grouping key for tables and PDF; the enum mirrors
|
||||
// the five DORA pillars declared in `prowler/compliance/dora.json`.
|
||||
export const DORA_PILLAR = {
|
||||
ICT_RISK_MANAGEMENT: "ICT Risk Management",
|
||||
INCIDENT_REPORTING: "ICT-Related Incident Reporting",
|
||||
RESILIENCE_TESTING: "Digital Operational Resilience Testing",
|
||||
THIRD_PARTY_RISK: "ICT Third-Party Risk Management",
|
||||
INFORMATION_SHARING: "Information Sharing",
|
||||
} as const;
|
||||
export type DORAPillar = (typeof DORA_PILLAR)[keyof typeof DORA_PILLAR];
|
||||
|
||||
export interface DORAAttributesMetadata {
|
||||
Pillar: DORAPillar;
|
||||
Article: string;
|
||||
ArticleTitle: string;
|
||||
}
|
||||
|
||||
export interface DORARequirement extends Requirement {
|
||||
pillar: DORAAttributesMetadata["Pillar"];
|
||||
article: DORAAttributesMetadata["Article"];
|
||||
article_title: DORAAttributesMetadata["ArticleTitle"];
|
||||
}
|
||||
|
||||
export interface AttributesItemData {
|
||||
type: "compliance-requirements-attributes";
|
||||
id: string;
|
||||
@@ -349,6 +374,7 @@ export interface AttributesItemData {
|
||||
| CCCAttributesMetadata[]
|
||||
| CSAAttributesMetadata[]
|
||||
| ASDEssentialEightAttributesMetadata[]
|
||||
| DORAAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
// MITRE structure
|
||||
|
||||
Reference in New Issue
Block a user