When to Use
Use this skill when:
- Creating a new compliance framework for any provider — decide universal vs legacy first (see below)
- Syncing an existing framework with an upstream source of truth (CIS, FINOS CCC, CSA CCM, NIST, ENS, etc.)
- Adding requirements to existing frameworks, or extending a universal framework to a new provider
- Mapping checks to compliance controls
- Adding
ConfigRequirements guardrails so configurable checks can't silently satisfy a requirement with a loosened config
- Auditing existing check mappings as a cloud auditor ("are these mappings correct?", "which checks apply?", "review the mappings")
- Adding a new legacy output formatter (table dispatcher + per-provider classes + CSV models)
- Fixing JSON bugs: duplicate IDs, empty Version, wrong Section, stale check refs, inconsistent FamilyName, padded tangential check mappings
- Investigating why a finding/check isn't showing under the expected compliance framework in the UI
- Understanding compliance framework structures and attributes
The authoritative contributor doc is docs/developer-guide/security-compliance-framework.mdx —
keep this skill and that doc consistent when either changes. For reviewing
a compliance PR, use the sister skill
prowler-compliance-review instead.
Universal vs Legacy: The First Decision
Prowler supports two JSON schemas. Choosing wrong means unnecessary Python
code, so decide this before anything else. At load time both converge: legacy
files are adapted into the universal ComplianceFramework model
(adapt_legacy_to_universal()), so the difference is about authoring cost
and capabilities, not about what the rest of Prowler sees.
Side-by-side comparison
|
Universal (recommended for new frameworks) |
Legacy provider-specific |
| File location |
prowler/compliance/<framework>.json (top level) |
prowler/compliance/<provider>/<framework>_<version>_<provider>.json |
| Providers |
Any number, one file (checks dict keyed by provider) |
Exactly one provider per file (one file per provider to multi-cover) |
| Key style |
lowercase (framework, requirements, checks) |
Capitalized (Framework, Requirements, Checks) |
| Attribute schema |
Declared in the JSON itself via attributes_metadata, validated at load |
Pydantic class per framework family in compliance_models.py (code change for new shapes) |
| Attributes per requirement |
One flat dict (attributes: {...}) |
List of objects (Attributes: [{...}]) — only Attributes[0] is used downstream |
| Table/CSV/OCSF output |
Data-driven from outputs.table_config — zero Python changes |
Formatter package + registrations in compliance.py, __main__.py, export.py |
| Guardrails field |
config_requirements (+ mandatory Provider per constraint) |
ConfigRequirements (Provider omitted) |
| Loader behavior on error |
Lenient: logs + skips file (load_compliance_framework_universal) |
Fail-fast: sys.exit(1) (load_compliance_framework) |
| Loaded by |
Only get_bulk_compliance_frameworks_universal() |
Both loaders (Compliance.get_bulk() + universal, via adapter) |
| Shipped examples |
cis_controls_8.1.json, csa_ccm_4.0.json, dora_2022_2554.json |
Everything else (~105 files across 11 providers) |
When to use which
Use universal when (any of these):
- The framework is new to Prowler — no existing attribute class, no
existing formatter. This is the default: zero Python changes needed.
- The framework spans (or will span) more than one provider — DORA, CSA
CCM, CIS Controls. One file covers all providers; extending to a new
provider is a one-line
checks edit.
- The attribute shape is unique to this framework — declare it in
attributes_metadata instead of adding a Pydantic class to the Union.
Use legacy only when extending an existing legacy family:
- A new version of a shipped legacy framework (CIS 8.0 for AWS → new
cis_8.0_aws.json, same CIS_Requirement_Attribute, same cis/ formatter).
- An existing legacy framework for a new provider (ENS for m365 → new
ens_rd2022_m365.json + ens_m365.py transformer).
- Consistency with the family matters more than the universal benefits — a
lone
cis_8.0_aws in universal format while 20+ CIS files stay legacy
would fragment the family.
Never: start a brand-new single-provider framework as legacy "because it's
only AWS today". Universal handles single-provider fine (the checks dict
just has one key) and you skip 3 output files + 3 registrations.
The same requirement in both schemas
Universal (prowler/compliance/my_framework_1.0.json):
{
"framework": "My-Framework",
"name": "My Framework 1.0",
"version": "1.0",
"description": "...",
"attributes_metadata": [
{"key": "Section", "type": "str", "required": true},
{"key": "Service", "type": "str"}
],
"outputs": {"table_config": {"group_by": "Section"}},
"requirements": [
{
"id": "MF-1.1",
"name": "Root MFA",
"description": "Root account must have MFA enabled.",
"attributes": {"Section": "IAM", "Service": "iam"},
"checks": {
"aws": ["iam_root_mfa_enabled"],
"azure": []
}
}
]
}
Legacy (prowler/compliance/aws/my_framework_1.0_aws.json — plus a second
file per extra provider, plus formatter + registrations):
{
"Framework": "My-Framework",
"Name": "My Framework 1.0 for AWS",
"Version": "1.0",
"Provider": "AWS",
"Description": "...",
"Requirements": [
{
"Id": "MF-1.1",
"Name": "Root MFA",
"Description": "Root account must have MFA enabled.",
"Attributes": [
{"ItemId": "MF-1.1", "Section": "IAM", "Service": "iam"}
],
"Checks": ["iam_root_mfa_enabled"]
}
]
}
Same control, but the universal file already covers Azure, validates its own
attribute schema, and renders table/CSV/OCSF with no code. Field-by-field
references for each schema follow below.
Architecture (Mental Model)
Prowler compliance is a four-layer system. Bugs usually happen where one layer
doesn't match another, so know all four before touching anything.
Layer 1: SDK / Core Models — prowler/lib/check/
All in Pydantic v1 (from pydantic.v1 import ...). Three model groups live
in compliance_models.py:
Legacy tree — Compliance → Compliance_Requirement / Mitre_Requirement:
- One
*_Requirement_Attribute class per framework family. Registered today (Union order matters):
ASDEssentialEight, CIS, ENS, ISO27001_2013, AWS_Well_Architected,
KISA_ISMSP, Prowler_ThreatScore, CCC, C5Germany, CSA_CCM, STIG
(Okta IDaaS), and Generic_Compliance_Requirement_Attribute as fallback.
- Generic MUST stay LAST in
Compliance_Requirement.Attributes: list[Union[...]] —
Pydantic v1 tries union members in order; Generic first would swallow every
framework-specific attribute. NIST 800-53/CSF, PCI DSS, GDPR, HIPAA, SOC2,
FedRAMP, SecNumCloud etc. intentionally use Generic.
- A
root_validator rejects empty Framework, Provider or Name.
- MITRE uses the separate
Mitre_Requirement model (Tactics, SubTechniques,
Platforms, TechniqueURL at requirement top level, per-provider
Mitre_Requirement_Attribute_{AWS,Azure,GCP}).
Universal tree — ComplianceFramework → UniversalComplianceRequirement:
- Flat
attributes: dict per requirement, schema declared in
attributes_metadata (key, label, type, enum, required, enum_display,
enum_order, output_formats). A root_validator rejects missing required
keys, unknown keys (drift guard), enum violations, and int/float/bool type
mismatches. If attributes_metadata is omitted, no validation runs.
checks: dict[provider, list[check_id]] — the provider list of the framework
is derived from these keys (get_providers() / supports_provider());
the top-level provider field is only a fallback.
outputs.table_config (group_by, split_by, scoring, labels) drives the CLI
table; outputs.pdf_config exists in the model but is not consumed by the
API PDF pipeline yet (see Layer 4).
Guardrails — Compliance_Requirement_ConfigConstraint:
- Fields
Check, ConfigKey, Operator (lte|gte|eq|in|subset|superset),
Value, optional Provider (required in universal multi-provider files).
- A
root_validator rejects Value/Operator type mismatches at load time.
- Evaluation is centralized in
prowler/lib/check/compliance_config_eval.py
(evaluate_config_constraints, apply_config_status, get_effective_status,
CONFIG_NOT_VALID_PREFIX = "Configuration not valid for this requirement."),
shared by CSV/OCSF/table outputs and the API backend. A violated
constraint forces the requirement to FAIL and prepends the reason to
status_extended. Constraints whose ConfigKey is absent from
audit_config are skipped (defaults assumed compliant).
Loaders:
Compliance.get_bulk(provider) — legacy: scans only
prowler/compliance/{provider}/ (+ external JSONs via the
prowler.compliance entry-point group). Does NOT see top-level universal files.
get_bulk_compliance_frameworks_universal(provider) — scans both the
top-level prowler/compliance/ and every provider subdirectory, adapting
legacy files via adapt_legacy_to_universal() (flattens Attributes[0] to a
dict, wraps Checks as {provider: [...]}, infers attributes_metadata).
Also loads external universal frameworks via the
prowler.compliance.universal entry-point group (built-ins win collisions).
get_check_compliance(finding, provider_type, bulk_checks_metadata) lives in
prowler/lib/outputs/compliance/compliance_check.py (not in
lib/check/compliance.py). It builds the per-finding dict keyed
f"{Framework}-{Version}" only when Version is non-empty — an empty
Version silently produces the key "{Framework}" and breaks downstream
filters and tests.
prowler/lib/check/compliance.py now contains only
update_checks_metadata_with_compliance().
Layer 2: JSON Catalogs — prowler/compliance/
See "Compliance Catalog Coverage" below.
Layer 3: Output Formatters — prowler/lib/outputs/compliance/
Universal path (no Python needed per framework):
universal/universal_table.py — get_universal_table(), renders the CLI
table from outputs.table_config + attributes_metadata.
universal/universal_output.py — UniversalComplianceOutput, builds the CSV
Pydantic model dynamically from attributes_metadata.
universal/ocsf_compliance.py — OCSFComplianceOutput; OCSF output is
always generated for universal frameworks regardless of --output-formats.
- Orchestrated by
process_universal_compliance_frameworks() in
compliance.py, which runs before any legacy dispatch and removes the
processed frameworks from the set.
Legacy path — per-framework directory, usually:
{framework}/
├── __init__.py
├── {framework}.py # get_{framework}_table() summary-table function
├── {framework}_{provider}.py # One ComplianceOutput subclass per provider
└── models.py # One Pydantic CSV row model per provider
Directories today: asd_essential_eight, aws_well_architected, c5, ccc,
cis, cisa_scuba, ens, generic, iso27001, kisa_ismsp,
mitre_attack, okta_idaas_stig, prowler_threatscore, universal.
Known deviations (don't "fix" them without a reason): iso27001/ has no table
file (falls to the generic table), aws_well_architected/ has no per-provider
files, cisa_scuba/ only ships googleworkspace.
- CSV writers emit
;-delimited files with UPPERCASE headers
(ComplianceOutput.batch_write_data_to_file). Field names in models.py
are public API — renaming breaks downstream consumers.
- Circular import rule: the table file (
{framework}.py) must not import
Finding directly or transitively (compliance.compliance → table module →
ComplianceOutput → Finding → get_check_compliance → cycle). Keep table
files bare (colorama, tabulate, prowler.config.config); when a module
genuinely needs both, use if TYPE_CHECKING: or function-local imports (see
universal_output.py / process_universal_compliance_frameworks).
- Legacy table functions have no docstrings; the universal ones do. Match the
style of the file family you're touching.
- Dispatcher
display_compliance_table() in compliance.py order:
universal (table_config) first → cis_ → ens_ → mitre_attack →
kisa → prowler_threatscore_ → c5_ → ccc_ → asd_essential_eight
(substring) → okta_idaas_stig → else provider hook
(provider.display_compliance_table(), may raise NotImplementedError) →
get_generic_compliance_table(). iso27001, aws_well_architected and
cisa_scuba ride the fallback on purpose.
Layer 4: API / UI
- API lazy loaders:
api/src/backend/api/compliance.py —
LazyComplianceTemplate / LazyChecksMapping (per-provider lazy caches over
get_bulk_compliance_frameworks_universal, with Gunicorn background warm-up).
- API CSV export dispatch:
COMPLIANCE_CLASS_MAP in
api/src/backend/tasks/jobs/export.py, consumed from tasks/tasks.py. It is
a dict provider → [(predicate, exporter_class)] with GenericCompliance as
fallback. Predicates mix startswith for multi-version families
(cis_, ens_, iso27001_, ccc_, cisa_scuba_, ...) and exact
name == ... for true singletons (mitre_attack_aws,
prowler_threatscore_*, asd_essential_eight_aws — and inconsistently
c5_azure/c5_gcp, while aws uses startswith("c5_")). Rule of thumb: if
the framework can ever grow versions or variants, use startswith.
- API overview ingestion:
create_compliance_requirements() in
api/src/backend/tasks/jobs/scan.py builds per-region rows from the lazy
template and persists ComplianceRequirementOverview (COPY with bulk-create
fallback) plus ComplianceOverviewSummary.
- API PDF reports:
api/src/backend/tasks/jobs/reports/ — hardcoded
FRAMEWORK_REGISTRY (own FrameworkConfig dataclass, NOT the SDK
PDFConfig) with one generator class per framework. Only
prowler_threatscore, ens, nis2, csa_ccm and cis have PDFs today;
adding one means a generator class + registry entry + wiring in report.py.
- UI mapper routing:
ui/lib/compliance/compliance-mapper.ts —
getComplianceMappers() keyed by the JSON's framework value
(e.g. "CIS", "CIS-Controls", "DORA", "Okta-IDaaS-STIG"). Unregistered
frameworks fall back to the generic mapper + GenericCustomDetails
automatically — a dedicated mapper/detail panel is a first-class upgrade,
not a requirement to render.
- UI grouping varies per mapper: generic/cis group by
Section/SubSection, iso by Category, ccc by FamilyName. All read
attributes[0] — inconsistent values within one JSON become separate tree
branches, so normalize before shipping.
- UI types:
ui/types/compliance.ts — one *AttributesMetadata interface
per framework, added to the AttributesItemData metadata union.
- UI icons:
ui/components/icons/compliance/ + IconCompliance.tsx.
Registration is an ordered substring match (COMPLIANCE_LOGOS): put
framework-specific keywords before generic ones (nist before nis2,
cisa before cis; aws deliberately last).
The CLI Pipeline (end-to-end)
prowler aws --compliance cis_7.0_aws # framework key = JSON basename
↓
Compliance.get_bulk("aws") # legacy frameworks
get_bulk_compliance_frameworks_universal("aws") # legacy (adapted) + universal
↓
update_checks_metadata_with_compliance() # attaches compliance to CheckMetadata
↓
execute_checks() → Finding objects
↓
get_check_compliance(finding, "aws", bulk) # dict "{Framework}-{Version}" → [req_ids]
↓
process_universal_compliance_frameworks() # universal: CSV + OCSF, then removed from set
per-provider elif branches in __main__.py # legacy: AWSCIS(...).batch_write_data_to_file()
↓
display_compliance_table() # universal table first, then legacy elifs,
# then generic fallback
Compliance Catalog Coverage
Counts as of 2026-07 (109 JSON files). Regenerate before trusting them:
for d in prowler/compliance/*/; do printf "%s: %s\n" "$(basename $d)" "$(ls $d*.json 2>/dev/null | wc -l)"; done
ls prowler/compliance/*.json # universal, top-level
Universal (top-level, multi-provider): cis_controls_8.1.json (18
providers), csa_ccm_4.0.json (aws/azure/gcp/alibabacloud/oraclecloud),
dora_2022_2554.json (aws/azure/gcp/alibabacloud/cloudflare).
Legacy per-provider (families, not exhaustive versions):
| Provider |
# |
Framework families |
| aws |
45 |
CIS 1.4–7.0, NIST 800-53 r4/r5, NIST 800-171 r2, NIST CSF 1.1/2.0, PCI 3.2.1/4.0, ISO 27001 2013/2022, HIPAA, GDPR, SOC2, FedRAMP low/moderate r4 + 20x KSI low, ENS RD2022, MITRE ATT&CK, C5, CCC, CISA, FFIEC, RBI, Well-Architected (security/reliability), FTR, FSBP, AWS AI Security Framework, AWS Account Security Onboarding, Audit Manager Control Tower, GxP 21 CFR 11 / EU Annex 11, KISA ISMS-P 2023 (en+ko), NIS2, ASD Essential Eight, SecNumCloud 3.2, Prowler ThreatScore |
| azure |
19 |
CIS 2.0–6.0, ISO 27001 2022, ENS RD2022, MITRE ATT&CK, PCI 4.0, HIPAA, SOC2, NIS2, RBI, C5, CCC, FedRAMP 20x KSI low, SecNumCloud 3.2, Prowler ThreatScore |
| gcp |
17 |
CIS 2.0–5.0, ISO 27001 2022, ENS RD2022, MITRE ATT&CK, PCI 4.0, HIPAA, SOC2, NIS2, RBI, C5, CCC, FedRAMP 20x KSI low, SecNumCloud 3.2, Prowler ThreatScore |
| kubernetes |
8 |
CIS 1.8–2.0.1, ISO 27001 2022, PCI 4.0, Prowler ThreatScore |
| m365 |
5 |
CIS 4.0/6.0/7.0, ISO 27001 2022, Prowler ThreatScore |
| alibabacloud |
3 |
CIS 2.0, SecNumCloud 3.2, Prowler ThreatScore |
| oraclecloud |
3 |
CIS 3.0/3.1, SecNumCloud 3.2 |
| github |
2 |
CIS 1.0/1.2.0 |
| googleworkspace |
2 |
CIS 1.3, CISA SCuBA 0.6 |
| okta |
1 |
Okta IDaaS STIG V1R2 |
| nhn |
1 |
ISO 27001 2022 |
Providers with a compliance directory but no frameworks yet: cloudflare, iac,
linode, llm, mongodbatlas, openstack, stackit. Provider keys inside universal
checks dicts must match directory names under prowler/providers/ (lowercase).
Universal Schema Reference
Full spec in docs/developer-guide/security-compliance-framework.mdx. Skeleton:
{
"framework": "DORA",
"name": "Digital Operational Resilience Act (DORA) 2022/2554",
"version": "2022/2554",
"description": "Shown in --list-compliance and PDF reports.",
"icon": "dora",
"attributes_metadata": [
{"key": "Pillar", "label": "Pillar", "type": "str", "required": true,
"enum": ["ICT Risk Management", "..."],
"output_formats": {"csv": true, "ocsf": true}},
{"key": "Article", "type": "str", "required": true}
],
"outputs": {
"table_config": {"group_by": "Pillar"},
"pdf_config": {"group_by_field": "Pillar", "charts": ["..."]}
},
"requirements": [
{
"id": "DORA-Art5",
"name": "Governance and organisation",
"description": "Requirement text verbatim from the source.",
"attributes": {"Pillar": "ICT Risk Management", "Article": "Article 5"},
"checks": {
"aws": ["iam_no_root_access_key"],
"azure": [],
"gcp": []
},
"config_requirements": [
{"Check": "iam_user_accesskey_unused", "Provider": "aws",
"ConfigKey": "max_unused_access_keys_days", "Operator": "lte", "Value": 45}
]
}
]
}
Universal fields, top level (ComplianceFramework)
| Field |
Type |
Required |
Notes |
framework |
string |
Yes |
Short identifier (DORA, CSA-CCM, CIS-Controls). This is the key the UI mapper routes on. |
name |
string |
Yes |
Human-readable full name. |
version |
string |
No (never leave empty) |
Framework version/edition (8.1, 2022/2554). |
description |
string |
Yes |
Shown in --list-compliance and PDF reports. |
provider |
string |
No |
Fallback only — the effective provider list is derived from checks keys across requirements (get_providers()). |
icon |
string |
No |
Short icon slug. |
attributes_metadata |
array |
No (strongly recommended) |
Declares the schema of every attributes key. If omitted, no attribute validation runs at all. |
outputs |
object |
No |
table_config (CLI table) + pdf_config (modeled, not yet consumed by the API). |
requirements |
array |
Yes |
List of requirement objects (below). |
Universal fields, per requirement (UniversalComplianceRequirement)
| Field |
Type |
Required |
Notes |
id |
string |
Yes |
Unique within the framework. |
description |
string |
Yes |
Requirement text verbatim from the source. |
name |
string |
No |
Short title. |
attributes |
dict |
No (default {}) |
Flat dict; every key must be declared in attributes_metadata (unknown keys are rejected at load when metadata exists). |
checks |
dict |
No (default {}) |
{provider: [check_ids]}, lowercase keys matching prowler/providers/ dirs. Empty list = manual requirement for that provider. |
config_requirements |
array |
No |
Guardrails; each constraint must carry Provider. |
tactics, sub_techniques, platforms, technique_url |
— |
No |
MITRE-style extras (auto-populated when adapting legacy MITRE files). |
attributes_metadata entry fields (AttributeMetadata)
| Field |
Type |
Notes |
key |
string (required) |
Attribute name as used in requirement.attributes. |
label |
string |
Human-readable label for CSV headers / PDF. |
type |
string |
str (default), int, float, bool, list_str, list_dict. Only int/float/bool are enforced at load; the rest are documentation. |
enum |
list |
Allowed values — enforced at load. Use it whenever the value set is closed. |
required |
bool |
Enforced at load: every requirement must carry the key non-null. |
enum_display / enum_order |
dict / list |
Per-enum-value visual metadata (label, abbreviation, color, icon) and ordering for PDF rendering. |
chart_label |
string |
Axis label when the attribute is used in charts. |
output_formats |
object |
{"csv": bool, "ocsf": bool}, both default true — toggles inclusion per output. |
Key rules:
--compliance key = JSON basename without .json (dora_2022_2554).
- Auto-discovered: no
__init__.py, no formatter, no dispatcher registration.
table_config.group_by, pdf_config.group_by_field and every
charts[].group_by must reference a key declared in attributes_metadata.
- Runtime type validation only covers
int/float/bool; str/list_str/
list_dict are documentation-only.
- Extending to a new provider = adding a key to
requirement.checks. Nothing else.
- No automatic check-existence validation at load time — a typo'd check id
silently produces a requirement with no findings. Always run the
check-existence cross-check (see Validation).
- In universal files, always set
Provider on every config constraint so a
guardrail authored for an AWS check never affects Azure/GCP scans of the
same requirement.
Legacy Schema Reference
Base legacy file structure:
{
"Framework": "FRAMEWORK_NAME",
"Name": "Full Framework Name with Version",
"Version": "X.X",
"Provider": "AWS",
"Description": "Framework description...",
"Requirements": [
{
"Id": "requirement_id",
"Name": "Optional requirement name",
"Description": "Requirement description",
"Attributes": [ ... ],
"Checks": ["check_name_1"],
"ConfigRequirements": [ ... ]
}
]
}
Legacy fields, top level (Compliance)
| Field |
Type |
Required |
Notes |
Framework |
string |
Yes (non-empty, validated) |
Canonical identifier (CIS, ENS, NIST-800-53-Revision-5). |
Name |
string |
Yes (non-empty, validated) |
Human-readable name with version. |
Version |
string |
Optional in the model — never leave it empty in practice |
Empty Version silently degrades the get_check_compliance() key to "{Framework}" (gotcha #4). Must match the version substring in the filename. |
Provider |
string |
Yes (non-empty, validated) |
Upper-cased single provider (AWS, AZURE, GCP, M365, ...). One file = one provider. |
Description |
string |
Yes |
Framework scope and purpose. |
Requirements |
array |
Yes |
Requirement objects (below), or Mitre_Requirement objects for MITRE files. |
Legacy fields, per requirement (Compliance_Requirement)
| Field |
Type |
Required |
Notes |
Id |
string |
Yes |
Unique within the framework; follow the source numbering exactly (1.1, A.5.1, CCC.Core.CN01.AR01). |
Description |
string |
Yes |
Verbatim from the source catalog. |
Name |
string |
No |
Optional short title (NIST-style catalogs use it). |
Attributes |
array of objects |
Yes |
Parsed against the Union of attribute classes below; only Attributes[0] survives the universal adaptation and drives UI grouping. |
Checks |
array of strings |
Yes |
Check ids automating the requirement; [] = manual. |
ConfigRequirements |
array |
No |
Guardrails; Provider is omitted (the file is single-provider). |
MITRE files use Mitre_Requirement instead, which adds Tactics,
SubTechniques, Platforms, TechniqueURL at the requirement top level.
Attribute shapes per framework family
Unlike universal (schema in-file), a legacy requirement's Attributes must
match one of the Pydantic classes registered in
Compliance_Requirement.Attributes — a shape matching no class silently
falls through to Generic, dropping its specific fields. The most common
shapes (full field sets in compliance_models.py):
CIS — cis_{version}_{provider}
{
"Section": "1 Identity and Access Management",
"SubSection": "Optional subsection",
"Profile": "Level 1",
"AssessmentStatus": "Automated",
"Description": "...", "RationaleStatement": "...", "ImpactStatement": "...",
"RemediationProcedure": "...", "AuditProcedure": "...",
"AdditionalInformation": "...", "DefaultValue": "...", "References": "https://..."
}
Profile: Level 1|Level 2|E3 Level 1|E3 Level 2|E5 Level 1|E5 Level 2.
AssessmentStatus: Automated|Manual.
ENS — ens_rd2022_{provider}
{
"IdGrupoControl": "op.acc.1", "Marco": "operacional",
"Categoria": "control de acceso", "DescripcionControl": "...",
"Nivel": "alto", "Tipo": "requisito",
"Dimensiones": ["trazabilidad", "autenticidad"],
"ModoEjecucion": "automatico", "Dependencias": []
}
Nivel: opcional|bajo|medio|alto. Tipo: refuerzo|requisito|recomendacion|medida.
Dimensiones: confidencialidad|integridad|trazabilidad|autenticidad|disponibilidad.
ISO 27001 — iso27001_{year}_{provider}
{
"Category": "A.5 Organizational controls",
"Objetive_ID": "A.5.1", "Objetive_Name": "Policies for information security",
"Check_Summary": "Summary of what is being checked"
}
Note: Objetive_ID / Objetive_Name use this exact (mis)spelling.
MITRE ATT&CK — mitre_attack_{provider} (separate requirement model)
{
"Name": "Exploit Public-Facing Application", "Id": "T1190",
"Tactics": ["Initial Access"], "SubTechniques": [],
"Platforms": ["IaaS"], "Description": "...",
"TechniqueURL": "https://attack.mitre.org/techniques/T1190/",
"Checks": ["guardduty_is_enabled"],
"Attributes": [
{"AWSService": "Amazon GuardDuty", "Category": "Detect",
"Value": "Minimal", "Comment": "..."}
]
}
AzureService/GCPService for the other providers. Category:
Detect|Protect|Respond. Value: Minimal|Partial|Significant.
CCC — ccc_{provider}
{
"FamilyName": "Data", "FamilyDescription": "...",
"Section": "CCC.Core.CN01 Encrypt Data for Transmission", "SubSection": "",
"SubSectionObjective": "...",
"Applicability": ["tlp-green", "tlp-amber", "tlp-red"],
"Recommendation": "...",
"SectionThreatMappings": [{"ReferenceId": "CCC", "Identifiers": ["CCC.Core.TH02"]}],
"SectionGuidelineMappings": [{"ReferenceId": "NIST-CSF", "Identifiers": ["PR.DS-02"]}]
}
Applicability holds TLP tags (tlp-clear|tlp-green|tlp-amber|tlp-red).
ASD Essential Eight — asd_essential_eight_aws
{
"Section": "Patch applications", "MaturityLevel": "ML1",
"AssessmentStatus": "Automated", "CloudApplicability": "partial",
"MitigatedThreats": ["..."], "Description": "...",
"RationaleStatement": "...", "ImpactStatement": "...",
"RemediationProcedure": "...", "AuditProcedure": "...",
"AdditionalInformation": "...", "References": "..."
}
MaturityLevel: ML1|ML2|ML3. CloudApplicability: full|partial|limited|non-applicable.
DISA STIG — okta_idaas_stig_v1r2_okta
{
"Section": "...", "Severity": "high", "RuleID": "...", "StigID": "...",
"CCI": ["CCI-000015"], "CheckText": "...", "FixText": "..."
}
Severity: high|medium|low (maps to CAT I/II/III).
Other registered shapes
- AWS Well-Architected (
aws_well_architected_framework_{pillar}_pillar_aws):
Name, WellArchitectedQuestionId, WellArchitectedPracticeId, Section,
SubSection, LevelOfRisk, AssessmentMethod, Description,
ImplementationGuidanceUrl.
- KISA ISMS-P (
kisa_isms_p_2023_{provider}): Domain, Subdomain,
Section, AuditChecklist, RelatedRegulations, AuditEvidence,
NonComplianceCases.
- C5 (
c5_{provider}): Section, SubSection, Type, AboutCriteria,
ComplementaryCriteria.
- CSA CCM (legacy shape; the shipped CSA CCM 4.0 is universal):
Section,
CCMLite, IaaS, PaaS, SaaS, ScopeApplicability.
- Prowler ThreatScore (
prowler_threatscore_{provider}): Title,
Section, SubSection, AttributeDescription, AdditionalInformation,
LevelOfRisk (1–5), Weight (1/8/10/100/1000). Pillars: 1 IAM, 2 Attack
Surface, 3 Logging and Monitoring, 4 Encryption. Available for aws,
azure, gcp, kubernetes, m365, alibabacloud.
- Generic (fallback):
ItemId, Section, SubSection, SubGroup,
Service, Type, Comment — all optional. Used by NIST, PCI, GDPR,
HIPAA, SOC2, FedRAMP, CISA, FFIEC, RBI, NIS2, GxP, SecNumCloud, etc.
Config Guardrails (ConfigRequirements)
Requirements backed by configurable checks
can be silently "satisfied" by a loosened audit_config (e.g. CIS demands
45-day unused credentials but the scan ran with max_unused_access_keys_days: 120).
Guardrails force such requirements to FAIL:
"ConfigRequirements": [
{"Check": "iam_user_accesskey_unused",
"ConfigKey": "max_unused_access_keys_days", "Operator": "lte", "Value": 45}
]
- Operators:
lte/gte (numeric thresholds), eq (toggles/exact — use JSON
booleans, not 0/1), in (scalar in allowed set), subset (allowlists —
widening breaks it), superset (denylists — removing an entry breaks it).
Value must be the strictest setting the control text tolerates.
ConfigKey must be spelled exactly as the check reads it; unknown keys are
silently skipped (defaults assumed OK).
- Guardrails only tighten (PASS→FAIL), never relax.
- Universal files: lowercase
config_requirements + mandatory Provider per
constraint.
- Tests:
tests/lib/check/compliance_config_eval_test.py,
compliance_config_constraint_model_test.py,
compliance_config_requirements_data_test.py, plus per-output tests under
tests/lib/outputs/compliance/.
Workflow A: Sync a Framework With an Upstream Catalog
Use when the framework is maintained upstream (CIS Benchmarks, FINOS CCC, CSA
CCM, NIST, ENS, etc.) and Prowler needs to catch up.
Step 1 — Cache the upstream source
Download every upstream file to a local cache so iterations don't hit the
network. For FINOS CCC:
mkdir -p /tmp/ccc_upstream
catalogs="core/ccc storage/object management/auditlog management/logging ..."
for p in $catalogs; do
safe=$(echo "$p" | tr '/' '_')
gh api "repos/finos/common-cloud-controls/contents/catalogs/$p/controls.yaml" \
-H "Accept: application/vnd.github.raw" > "/tmp/ccc_upstream/${safe}.yaml"
done
Step 2 — Run the generic sync runner against a framework config
The sync tooling is three layers, so adding a framework only takes a YAML
config (plus a parser module for an unfamiliar upstream format):
skills/prowler-compliance/assets/
├── sync_framework.py # generic runner — works for any framework
├── configs/ccc.yaml # per-framework config (canonical example)
└── parsers/finos_ccc.py # parser module for FINOS CCC YAML
python skills/prowler-compliance/assets/sync_framework.py \
skills/prowler-compliance/assets/configs/ccc.yaml
The runner loads the config, dynamically imports parser.module, calls
parse_upstream(config) -> list[dict], then applies generic post-processing
(id-uniqueness safety net, FamilyName normalization, legacy check-mapping
preservation with config-driven fallback keys) and writes the provider JSONs
with Pydantic post-validation.
To add a new framework sync:
- Write
assets/configs/{framework}.yaml (see ccc.yaml). Required sections:
framework — name, display_name, version (never empty — the
runner refuses to start, because empty Version breaks the
get_check_compliance() key), description_template.
providers — list of {key, display} pairs.
output.path_template — e.g.
"prowler/compliance/{provider}/cis_{version}_{provider}.json".
upstream.dir — local cache (Step 1).
parser.module — module under parsers/; the rest of parser. is
passed through opaque.
post_processing.check_preservation.primary_key (almost always Id) and
fallback_keys — lists of Attributes[0] field names composed into
tuples for recovering mappings when ids change. CCC:
- [Section, Applicability]; CIS: - [Section, Profile]; NIST:
- [ItemId]. List-valued fields are frozen to frozenset automatically.
post_processing.family_name_normalization (optional) — raw → canonical
map; the UI groups by the exact attribute value, so upstream variants
otherwise become separate tree branches.
- Reuse an existing parser or write
parsers/{name}.py implementing
parse_upstream(config) -> list[dict] returning Prowler-format
requirements with guaranteed-unique ids. The runner raises on
duplicates — it never silently renumbers, because mutating a canonical
upstream id (CIS 1.1.1, NIST AC-2(1)) would be catastrophic. The parser
owns all upstream quirks: foreign-prefix rewriting, genuine collision
renumbering, multi-shape handling.
Gotchas the runner already handles (from the FINOS CCC v2025.10 sync):
- Multiple upstream YAML shapes. Most FINOS CCC catalogs use
control-families: [...] but storage/object uses top-level
controls: [...]. A single-shape parser silently drops entire catalogs —
this exact bug dropped ObjStor for a full iteration. Test with one file of
each shape.
- Whitespace collapse. Upstream
| block scalars keep newlines; Prowler
stores single-line. Collapse with " ".join(value.split()).
- Foreign-prefix id rewriting. Upstream aliases requirements across
catalogs keeping the original prefix (
CCC.AuditLog.CN08.AR01 nested under
CCC.Logging.CN03) — rewrite to fit the parent (CCC.Logging.CN03.AR01).
- Genuine upstream collisions. Two different requirements sharing one id
(upstream typo): renumber the second to the next free number; check-mapping
preservation recovers by the fallback keys.
- Populate
Version — fail-fast beats the silent broken-key bug.
Step 3 — Validate before committing
Run the full Validation section below (universal loader + check existence +
CLI smoke + pytest).
Step 4 — Add an attribute model if needed
Only if the framework has fields beyond
Generic_Compliance_Requirement_Attribute and must stay legacy. Add the class
to compliance_models.py and register it in the
Compliance_Requirement.Attributes Union before Generic (Generic stays
last). For new frameworks, prefer universal attributes_metadata instead.
Workflow B: Audit Check Mappings as a Cloud Auditor
Use when the user asks to review existing mappings. This is the
highest-value compliance task — it surfaces padded mappings with zero actual
coverage and missing mappings for legitimate coverage.
The golden rule
A Prowler check's title/risk MUST literally describe what the requirement
text says. "Related" is not enough. If no check actually addresses the
requirement, leave the checks list empty (MANUAL) — honest MANUAL is worth
more than padded coverage.
Audit process
Build a per-provider check inventory — assets/build_inventory.py
(writes /tmp/checks_{provider}.json for every provider discovered under
prowler/providers/).
Query it — assets/query_checks.py (run from the repository root):
python skills/prowler-compliance/assets/query_checks.py aws encryption transit # keyword AND-search
python skills/prowler-compliance/assets/query_checks.py aws --service iam # all iam checks
python skills/prowler-compliance/assets/query_checks.py aws --id kms_cmk_rotation_enabled
Dump a framework section with current mappings — assets/dump_section.py:
python skills/prowler-compliance/assets/dump_section.py ccc "CCC.Core."
python skills/prowler-compliance/assets/dump_section.py cis_5.0_aws "1."
Encode explicit REPLACE decisions — assets/audit_framework_template.py:
DECISIONS = {}
DECISIONS["CCC.Core.CN01.AR01"] = {
"aws": ["cloudfront_distributions_https_enabled", ...],
"azure": ["storage_secure_transfer_required_is_enabled", ...],
"gcp": ["cloudsql_instance_ssl_connections"],
# Missing provider key = leave the legacy mapping untouched
}
# Empty list = EXPLICITLY MANUAL (overwrites legacy)
DECISIONS["CCC.Core.CN01.AR07"] = {"aws": [], "azure": [], "gcp": []}
REPLACE, not PATCH. Full lists make the audit reproducible and surface
hidden assumptions in the legacy data.
Pre-validate every check id against the inventory; the script MUST
abort with stderr listing typos (real audits caught
storage_secure_transfer_required_enabled →
storage_secure_transfer_required_is_enabled,
sqlserver_minimum_tls_version_12 →
sqlserver_recommended_minimal_tls_version, and several checks that
simply don't exist).
Apply + validate + test:
python /path/to/audit_script.py
uv run pytest -n auto tests/lib/outputs/compliance/ tests/lib/check/ -q
For the curated mapping table (requirement text → AWS/Azure/GCP checks) and
the list of controls Prowler genuinely cannot verify, see
references/check-mapping-reference.md.
Workflow C: Add a New Universal Framework
- Author
prowler/compliance/{framework}_{version}.json following the
Universal Schema Reference above (use dora_2022_2554.json or
csa_ccm_4.0.json as template).
- Declare every attribute in
attributes_metadata (with required/enum
where possible — that's your load-time validation) and a
outputs.table_config.group_by.
- Map checks per provide
…(truncated)
1---2name: prowler-compliance3description: Creates, syncs, audits and manages Prowler compliance frameworks end-to-end. Covers the two supported JSON schemas (universal multi-provider and legacy per-provider), the SDK model tree (legacy attribute classes, universal ComplianceFramework, ConfigRequirements guardrails), output formatters (legacy per-framework + universal data-driven), API/UI consumption, upstream sync workflows, and cloud-auditor check-mapping reviews. Trigger: When working with compliance frameworks (CIS, CIS Controls, NIST, PCI-DSS, SOC2, GDPR, ISO27001, ENS, MITRE ATT&CK, CCC, C5, CSA CCM, DORA, KISA ISMS-P, ASD Essential Eight, DISA STIG, CISA SCuBA, SecNumCloud, FedRAMP, HIPAA, NIS2, Prowler ThreatScore), creating a universal multi-provider framework, adding ConfigRequirements guardrails, syncing with upstream catalogs, auditing check-to-requirement mappings, adding output formatters, or fixing compliance JSON bugs (duplicate IDs, empty Version, wrong Section, stale check refs).4license: Apache-2.05---6
7## When to Use
8
9Use this skill when:
10
11- Creating a new compliance framework for any provider — **decide universal vs legacy first** (see below)
12- **Syncing an existing framework with an upstream source of truth** (CIS, FINOS CCC, CSA CCM, NIST, ENS, etc.)
13- Adding requirements to existing frameworks, or extending a universal framework to a new provider
14- Mapping checks to compliance controls
15- **Adding `ConfigRequirements` guardrails** so configurable checks can't silently satisfy a requirement with a loosened config
16- **Auditing existing check mappings as a cloud auditor** ("are these mappings correct?", "which checks apply?", "review the mappings")
17- **Adding a new legacy output formatter** (table dispatcher + per-provider classes + CSV models)
18- **Fixing JSON bugs**: duplicate IDs, empty Version, wrong Section, stale check refs, inconsistent FamilyName, padded tangential check mappings
19- Investigating why a finding/check isn't showing under the expected compliance framework in the UI
20- Understanding compliance framework structures and attributes
21
22The authoritative contributor doc is `docs/developer-guide/security-compliance-framework.mdx` —
23keep this skill and that doc consistent when either changes. For **reviewing**
24a compliance PR, use the sister skill
25[prowler-compliance-review](../prowler-compliance-review/SKILL.md) instead.
26
27## Universal vs Legacy: The First Decision
28
29Prowler supports **two JSON schemas**. Choosing wrong means unnecessary Python
30code, so decide this before anything else. At load time both converge: legacy
31files are adapted into the universal `ComplianceFramework` model
32(`adapt_legacy_to_universal()`), so the difference is about **authoring cost
33and capabilities**, not about what the rest of Prowler sees.
34
35### Side-by-side comparison
36
37| | Universal (recommended for new frameworks) | Legacy provider-specific |
38|---|---|---|
39| File location | `prowler/compliance/<framework>.json` (top level) | `prowler/compliance/<provider>/<framework>_<version>_<provider>.json` |
40| Providers | Any number, one file (`checks` dict keyed by provider) | Exactly one provider per file (one file per provider to multi-cover) |
41| Key style | lowercase (`framework`, `requirements`, `checks`) | Capitalized (`Framework`, `Requirements`, `Checks`) |
42| Attribute schema | Declared **in the JSON itself** via `attributes_metadata`, validated at load | Pydantic class per framework family in `compliance_models.py` (code change for new shapes) |
43| Attributes per requirement | One flat dict (`attributes: {...}`) | List of objects (`Attributes: [{...}]`) — only `Attributes[0]` is used downstream |
44| Table/CSV/OCSF output | Data-driven from `outputs.table_config` — **zero Python changes** | Formatter package + registrations in `compliance.py`, `__main__.py`, `export.py` |
45| Guardrails field | `config_requirements` (+ mandatory `Provider` per constraint) | `ConfigRequirements` (`Provider` omitted) |
46| Loader behavior on error | Lenient: logs + skips file (`load_compliance_framework_universal`) | Fail-fast: `sys.exit(1)` (`load_compliance_framework`) |
47| Loaded by | Only `get_bulk_compliance_frameworks_universal()` | Both loaders (`Compliance.get_bulk()` + universal, via adapter) |
48| Shipped examples | `cis_controls_8.1.json`, `csa_ccm_4.0.json`, `dora_2022_2554.json` | Everything else (~105 files across 11 providers) |
49
50### When to use which
51
52**Use universal when** (any of these):
53
54- The framework is **new to Prowler** — no existing attribute class, no
55 existing formatter. This is the default: zero Python changes needed.
56- The framework spans (or will span) **more than one provider** — DORA, CSA
57 CCM, CIS Controls. One file covers all providers; extending to a new
58 provider is a one-line `checks` edit.
59- The attribute shape is **unique to this framework** — declare it in
60 `attributes_metadata` instead of adding a Pydantic class to the Union.
61
62**Use legacy only when extending an existing legacy family**:
63
64- A new **version** of a shipped legacy framework (CIS 8.0 for AWS → new
65 `cis_8.0_aws.json`, same `CIS_Requirement_Attribute`, same `cis/` formatter).
66- An existing legacy framework for a **new provider** (ENS for m365 → new
67 `ens_rd2022_m365.json` + `ens_m365.py` transformer).
68- Consistency with the family matters more than the universal benefits — a
69 lone `cis_8.0_aws` in universal format while 20+ CIS files stay legacy
70 would fragment the family.
71
72**Never**: start a brand-new single-provider framework as legacy "because it's
73only AWS today". Universal handles single-provider fine (the `checks` dict
74just has one key) and you skip 3 output files + 3 registrations.
75
76### The same requirement in both schemas
77
78Universal (`prowler/compliance/my_framework_1.0.json`):
79
80```json
81{
82 "framework": "My-Framework",
83 "name": "My Framework 1.0",
84 "version": "1.0",
85 "description": "...",
86 "attributes_metadata": [
87 {"key": "Section", "type": "str", "required": true},
88 {"key": "Service", "type": "str"}
89 ],
90 "outputs": {"table_config": {"group_by": "Section"}},
91 "requirements": [
92 {
93 "id": "MF-1.1",
94 "name": "Root MFA",
95 "description": "Root account must have MFA enabled.",
96 "attributes": {"Section": "IAM", "Service": "iam"},
97 "checks": {
98 "aws": ["iam_root_mfa_enabled"],
99 "azure": []
100 }
101 }
102 ]
103}
104```
105
106Legacy (`prowler/compliance/aws/my_framework_1.0_aws.json` — plus a second
107file per extra provider, plus formatter + registrations):
108
109```json
110{
111 "Framework": "My-Framework",
112 "Name": "My Framework 1.0 for AWS",
113 "Version": "1.0",
114 "Provider": "AWS",
115 "Description": "...",
116 "Requirements": [
117 {
118 "Id": "MF-1.1",
119 "Name": "Root MFA",
120 "Description": "Root account must have MFA enabled.",
121 "Attributes": [
122 {"ItemId": "MF-1.1", "Section": "IAM", "Service": "iam"}
123 ],
124 "Checks": ["iam_root_mfa_enabled"]
125 }
126 ]
127}
128```
129
130Same control, but the universal file already covers Azure, validates its own
131attribute schema, and renders table/CSV/OCSF with no code. Field-by-field
132references for each schema follow below.
133
134## Architecture (Mental Model)
135
136Prowler compliance is a four-layer system. Bugs usually happen where one layer
137doesn't match another, so know all four before touching anything.
138
139### Layer 1: SDK / Core Models — `prowler/lib/check/`
140
141All in **Pydantic v1** (`from pydantic.v1 import ...`). Three model groups live
142in `compliance_models.py`:
143
144**Legacy tree** — `Compliance` → `Compliance_Requirement` / `Mitre_Requirement`:
145
146- One `*_Requirement_Attribute` class per framework family. Registered today (Union order matters):
147 `ASDEssentialEight`, `CIS`, `ENS`, `ISO27001_2013`, `AWS_Well_Architected`,
148 `KISA_ISMSP`, `Prowler_ThreatScore`, `CCC`, `C5Germany`, `CSA_CCM`, `STIG`
149 (Okta IDaaS), and `Generic_Compliance_Requirement_Attribute` as fallback.
150- **Generic MUST stay LAST** in `Compliance_Requirement.Attributes: list[Union[...]]` —
151 Pydantic v1 tries union members in order; Generic first would swallow every
152 framework-specific attribute. NIST 800-53/CSF, PCI DSS, GDPR, HIPAA, SOC2,
153 FedRAMP, SecNumCloud etc. intentionally use Generic.
154- A `root_validator` rejects empty `Framework`, `Provider` or `Name`.
155- MITRE uses the separate `Mitre_Requirement` model (`Tactics`, `SubTechniques`,
156 `Platforms`, `TechniqueURL` at requirement top level, per-provider
157 `Mitre_Requirement_Attribute_{AWS,Azure,GCP}`).
158
159**Universal tree** — `ComplianceFramework` → `UniversalComplianceRequirement`:
160
161- Flat `attributes: dict` per requirement, schema declared in
162 `attributes_metadata` (key, label, type, enum, required, `enum_display`,
163 `enum_order`, `output_formats`). A `root_validator` rejects missing required
164 keys, unknown keys (drift guard), enum violations, and int/float/bool type
165 mismatches. If `attributes_metadata` is omitted, **no validation runs**.
166- `checks: dict[provider, list[check_id]]` — the provider list of the framework
167 is **derived** from these keys (`get_providers()` / `supports_provider()`);
168 the top-level `provider` field is only a fallback.
169- `outputs.table_config` (group_by, split_by, scoring, labels) drives the CLI
170 table; `outputs.pdf_config` exists in the model but **is not consumed by the
171 API PDF pipeline yet** (see Layer 4).
172
173**Guardrails** — `Compliance_Requirement_ConfigConstraint`:
174
175- Fields `Check`, `ConfigKey`, `Operator` (`lte|gte|eq|in|subset|superset`),
176 `Value`, optional `Provider` (required in universal multi-provider files).
177- A `root_validator` rejects Value/Operator type mismatches at load time.
178- Evaluation is centralized in `prowler/lib/check/compliance_config_eval.py`
179 (`evaluate_config_constraints`, `apply_config_status`, `get_effective_status`,
180 `CONFIG_NOT_VALID_PREFIX = "Configuration not valid for this requirement."`),
181 shared by CSV/OCSF/table outputs **and** the API backend. A violated
182 constraint forces the requirement to FAIL and prepends the reason to
183 `status_extended`. Constraints whose `ConfigKey` is absent from
184 `audit_config` are skipped (defaults assumed compliant).
185
186**Loaders**:
187
188- `Compliance.get_bulk(provider)` — legacy: scans only
189 `prowler/compliance/{provider}/` (+ external JSONs via the
190 `prowler.compliance` entry-point group). Does NOT see top-level universal files.
191- `get_bulk_compliance_frameworks_universal(provider)` — scans **both** the
192 top-level `prowler/compliance/` and every provider subdirectory, adapting
193 legacy files via `adapt_legacy_to_universal()` (flattens `Attributes[0]` to a
194 dict, wraps `Checks` as `{provider: [...]}`, infers `attributes_metadata`).
195 Also loads external universal frameworks via the
196 `prowler.compliance.universal` entry-point group (built-ins win collisions).
197- `get_check_compliance(finding, provider_type, bulk_checks_metadata)` lives in
198 **`prowler/lib/outputs/compliance/compliance_check.py`** (not in
199 `lib/check/compliance.py`). It builds the per-finding dict keyed
200 `f"{Framework}-{Version}"` **only when Version is non-empty** — an empty
201 Version silently produces the key `"{Framework}"` and breaks downstream
202 filters and tests.
203- `prowler/lib/check/compliance.py` now contains only
204 `update_checks_metadata_with_compliance()`.
205
206### Layer 2: JSON Catalogs — `prowler/compliance/`
207
208See "Compliance Catalog Coverage" below.
209
210### Layer 3: Output Formatters — `prowler/lib/outputs/compliance/`
211
212**Universal path** (no Python needed per framework):
213
214- `universal/universal_table.py` — `get_universal_table()`, renders the CLI
215 table from `outputs.table_config` + `attributes_metadata`.
216- `universal/universal_output.py` — `UniversalComplianceOutput`, builds the CSV
217 Pydantic model **dynamically** from `attributes_metadata`.
218- `universal/ocsf_compliance.py` — `OCSFComplianceOutput`; OCSF output is
219 **always generated** for universal frameworks regardless of `--output-formats`.
220- Orchestrated by `process_universal_compliance_frameworks()` in
221 `compliance.py`, which runs **before** any legacy dispatch and removes the
222 processed frameworks from the set.
223
224**Legacy path** — per-framework directory, usually:
225
226```text
227{framework}/
228├── __init__.py
229├── {framework}.py # get_{framework}_table() summary-table function
230├── {framework}_{provider}.py # One ComplianceOutput subclass per provider
231└── models.py # One Pydantic CSV row model per provider
232```
233
234Directories today: `asd_essential_eight`, `aws_well_architected`, `c5`, `ccc`,
235`cis`, `cisa_scuba`, `ens`, `generic`, `iso27001`, `kisa_ismsp`,
236`mitre_attack`, `okta_idaas_stig`, `prowler_threatscore`, `universal`.
237Known deviations (don't "fix" them without a reason): `iso27001/` has no table
238file (falls to the generic table), `aws_well_architected/` has no per-provider
239files, `cisa_scuba/` only ships googleworkspace.
240
241- CSV writers emit `;`-delimited files with UPPERCASE headers
242 (`ComplianceOutput.batch_write_data_to_file`). Field names in `models.py`
243 are **public API** — renaming breaks downstream consumers.
244- **Circular import rule**: the table file (`{framework}.py`) must not import
245 `Finding` directly or transitively (`compliance.compliance` → table module →
246 `ComplianceOutput` → `Finding` → `get_check_compliance` → cycle). Keep table
247 files bare (`colorama`, `tabulate`, `prowler.config.config`); when a module
248 genuinely needs both, use `if TYPE_CHECKING:` or function-local imports (see
249 `universal_output.py` / `process_universal_compliance_frameworks`).
250- Legacy table functions have no docstrings; the universal ones do. Match the
251 style of the file family you're touching.
252- Dispatcher `display_compliance_table()` in `compliance.py` order:
253 universal (`table_config`) first → `cis_` → `ens_` → `mitre_attack` →
254 `kisa` → `prowler_threatscore_` → `c5_` → `ccc_` → `asd_essential_eight`
255 (substring) → `okta_idaas_stig` → else provider hook
256 (`provider.display_compliance_table()`, may raise `NotImplementedError`) →
257 `get_generic_compliance_table()`. iso27001, aws_well_architected and
258 cisa_scuba ride the fallback on purpose.
259
260### Layer 4: API / UI
261
262- **API lazy loaders**: `api/src/backend/api/compliance.py` —
263 `LazyComplianceTemplate` / `LazyChecksMapping` (per-provider lazy caches over
264 `get_bulk_compliance_frameworks_universal`, with Gunicorn background warm-up).
265- **API CSV export dispatch**: `COMPLIANCE_CLASS_MAP` in
266 `api/src/backend/tasks/jobs/export.py`, consumed from `tasks/tasks.py`. It is
267 a dict `provider → [(predicate, exporter_class)]` with `GenericCompliance` as
268 fallback. Predicates mix **`startswith` for multi-version families**
269 (`cis_`, `ens_`, `iso27001_`, `ccc_`, `cisa_scuba_`, ...) and **exact
270 `name == ...` for true singletons** (`mitre_attack_aws`,
271 `prowler_threatscore_*`, `asd_essential_eight_aws` — and inconsistently
272 `c5_azure`/`c5_gcp`, while aws uses `startswith("c5_")`). Rule of thumb: if
273 the framework can ever grow versions or variants, use `startswith`.
274- **API overview ingestion**: `create_compliance_requirements()` in
275 `api/src/backend/tasks/jobs/scan.py` builds per-region rows from the lazy
276 template and persists `ComplianceRequirementOverview` (COPY with bulk-create
277 fallback) plus `ComplianceOverviewSummary`.
278- **API PDF reports**: `api/src/backend/tasks/jobs/reports/` — hardcoded
279 `FRAMEWORK_REGISTRY` (own `FrameworkConfig` dataclass, NOT the SDK
280 `PDFConfig`) with one generator class per framework. Only
281 `prowler_threatscore`, `ens`, `nis2`, `csa_ccm` and `cis` have PDFs today;
282 adding one means a generator class + registry entry + wiring in `report.py`.
283- **UI mapper routing**: `ui/lib/compliance/compliance-mapper.ts` —
284 `getComplianceMappers()` keyed by the JSON's `framework` value
285 (e.g. `"CIS"`, `"CIS-Controls"`, `"DORA"`, `"Okta-IDaaS-STIG"`). Unregistered
286 frameworks **fall back to the generic mapper + `GenericCustomDetails`
287 automatically** — a dedicated mapper/detail panel is a first-class upgrade,
288 not a requirement to render.
289- **UI grouping varies per mapper**: generic/cis group by
290 `Section`/`SubSection`, iso by `Category`, ccc by `FamilyName`. All read
291 `attributes[0]` — inconsistent values within one JSON become separate tree
292 branches, so normalize before shipping.
293- **UI types**: `ui/types/compliance.ts` — one `*AttributesMetadata` interface
294 per framework, added to the `AttributesItemData` metadata union.
295- **UI icons**: `ui/components/icons/compliance/` + `IconCompliance.tsx`.
296 Registration is an ordered substring match (`COMPLIANCE_LOGOS`): put
297 framework-specific keywords **before** generic ones (`nist` before `nis2`,
298 `cisa` before `cis`; `aws` deliberately last).
299
300### The CLI Pipeline (end-to-end)
301
302```text
303prowler aws --compliance cis_7.0_aws # framework key = JSON basename
304 ↓
305Compliance.get_bulk("aws") # legacy frameworks
306get_bulk_compliance_frameworks_universal("aws") # legacy (adapted) + universal
307 ↓
308update_checks_metadata_with_compliance() # attaches compliance to CheckMetadata
309 ↓
310execute_checks() → Finding objects
311 ↓
312get_check_compliance(finding, "aws", bulk) # dict "{Framework}-{Version}" → [req_ids]
313 ↓
314process_universal_compliance_frameworks() # universal: CSV + OCSF, then removed from set
315per-provider elif branches in __main__.py # legacy: AWSCIS(...).batch_write_data_to_file()
316 ↓
317display_compliance_table() # universal table first, then legacy elifs,
318 # then generic fallback
319```
320
321---
322
323## Compliance Catalog Coverage
324
325Counts as of 2026-07 (109 JSON files). Regenerate before trusting them:
326
327```bash
328for d in prowler/compliance/*/; do printf "%s: %s\n" "$(basename $d)" "$(ls $d*.json 2>/dev/null | wc -l)"; done
329ls prowler/compliance/*.json # universal, top-level
330```
331
332**Universal (top-level, multi-provider)**: `cis_controls_8.1.json` (18
333providers), `csa_ccm_4.0.json` (aws/azure/gcp/alibabacloud/oraclecloud),
334`dora_2022_2554.json` (aws/azure/gcp/alibabacloud/cloudflare).
335
336**Legacy per-provider** (families, not exhaustive versions):
337
338| Provider | # | Framework families |
339|---|---|---|
340| aws | 45 | CIS 1.4–7.0, NIST 800-53 r4/r5, NIST 800-171 r2, NIST CSF 1.1/2.0, PCI 3.2.1/4.0, ISO 27001 2013/2022, HIPAA, GDPR, SOC2, FedRAMP low/moderate r4 + 20x KSI low, ENS RD2022, MITRE ATT&CK, C5, CCC, CISA, FFIEC, RBI, Well-Architected (security/reliability), FTR, FSBP, AWS AI Security Framework, AWS Account Security Onboarding, Audit Manager Control Tower, GxP 21 CFR 11 / EU Annex 11, KISA ISMS-P 2023 (en+ko), NIS2, ASD Essential Eight, SecNumCloud 3.2, Prowler ThreatScore |
341| azure | 19 | CIS 2.0–6.0, ISO 27001 2022, ENS RD2022, MITRE ATT&CK, PCI 4.0, HIPAA, SOC2, NIS2, RBI, C5, CCC, FedRAMP 20x KSI low, SecNumCloud 3.2, Prowler ThreatScore |
342| gcp | 17 | CIS 2.0–5.0, ISO 27001 2022, ENS RD2022, MITRE ATT&CK, PCI 4.0, HIPAA, SOC2, NIS2, RBI, C5, CCC, FedRAMP 20x KSI low, SecNumCloud 3.2, Prowler ThreatScore |
343| kubernetes | 8 | CIS 1.8–2.0.1, ISO 27001 2022, PCI 4.0, Prowler ThreatScore |
344| m365 | 5 | CIS 4.0/6.0/7.0, ISO 27001 2022, Prowler ThreatScore |
345| alibabacloud | 3 | CIS 2.0, SecNumCloud 3.2, Prowler ThreatScore |
346| oraclecloud | 3 | CIS 3.0/3.1, SecNumCloud 3.2 |
347| github | 2 | CIS 1.0/1.2.0 |
348| googleworkspace | 2 | CIS 1.3, CISA SCuBA 0.6 |
349| okta | 1 | Okta IDaaS STIG V1R2 |
350| nhn | 1 | ISO 27001 2022 |
351
352Providers with a compliance directory but no frameworks yet: cloudflare, iac,
353linode, llm, mongodbatlas, openstack, stackit. Provider keys inside universal
354`checks` dicts must match directory names under `prowler/providers/` (lowercase).
355
356---
357
358## Universal Schema Reference
359
360Full spec in `docs/developer-guide/security-compliance-framework.mdx`. Skeleton:
361
362```json
363{
364 "framework": "DORA",
365 "name": "Digital Operational Resilience Act (DORA) 2022/2554",
366 "version": "2022/2554",
367 "description": "Shown in --list-compliance and PDF reports.",
368 "icon": "dora",
369 "attributes_metadata": [
370 {"key": "Pillar", "label": "Pillar", "type": "str", "required": true,
371 "enum": ["ICT Risk Management", "..."],
372 "output_formats": {"csv": true, "ocsf": true}},
373 {"key": "Article", "type": "str", "required": true}
374 ],
375 "outputs": {
376 "table_config": {"group_by": "Pillar"},
377 "pdf_config": {"group_by_field": "Pillar", "charts": ["..."]}
378 },
379 "requirements": [
380 {
381 "id": "DORA-Art5",
382 "name": "Governance and organisation",
383 "description": "Requirement text verbatim from the source.",
384 "attributes": {"Pillar": "ICT Risk Management", "Article": "Article 5"},
385 "checks": {
386 "aws": ["iam_no_root_access_key"],
387 "azure": [],
388 "gcp": []
389 },
390 "config_requirements": [
391 {"Check": "iam_user_accesskey_unused", "Provider": "aws",
392 "ConfigKey": "max_unused_access_keys_days", "Operator": "lte", "Value": 45}
393 ]
394 }
395 ]
396}
397```
398
399### Universal fields, top level (`ComplianceFramework`)
400
401| Field | Type | Required | Notes |
402|---|---|---|---|
403| `framework` | string | Yes | Short identifier (`DORA`, `CSA-CCM`, `CIS-Controls`). This is the key the UI mapper routes on. |
404| `name` | string | Yes | Human-readable full name. |
405| `version` | string | No (never leave empty) | Framework version/edition (`8.1`, `2022/2554`). |
406| `description` | string | Yes | Shown in `--list-compliance` and PDF reports. |
407| `provider` | string | No | Fallback only — the effective provider list is derived from `checks` keys across requirements (`get_providers()`). |
408| `icon` | string | No | Short icon slug. |
409| `attributes_metadata` | array | No (strongly recommended) | Declares the schema of every `attributes` key. **If omitted, no attribute validation runs at all.** |
410| `outputs` | object | No | `table_config` (CLI table) + `pdf_config` (modeled, not yet consumed by the API). |
411| `requirements` | array | Yes | List of requirement objects (below). |
412
413### Universal fields, per requirement (`UniversalComplianceRequirement`)
414
415| Field | Type | Required | Notes |
416|---|---|---|---|
417| `id` | string | Yes | Unique within the framework. |
418| `description` | string | Yes | Requirement text verbatim from the source. |
419| `name` | string | No | Short title. |
420| `attributes` | dict | No (default `{}`) | Flat dict; every key must be declared in `attributes_metadata` (unknown keys are rejected at load when metadata exists). |
421| `checks` | dict | No (default `{}`) | `{provider: [check_ids]}`, lowercase keys matching `prowler/providers/` dirs. Empty list = manual requirement for that provider. |
422| `config_requirements` | array | No | Guardrails; each constraint **must** carry `Provider`. |
423| `tactics`, `sub_techniques`, `platforms`, `technique_url` | — | No | MITRE-style extras (auto-populated when adapting legacy MITRE files). |
424
425### `attributes_metadata` entry fields (`AttributeMetadata`)
426
427| Field | Type | Notes |
428|---|---|---|
429| `key` | string (required) | Attribute name as used in `requirement.attributes`. |
430| `label` | string | Human-readable label for CSV headers / PDF. |
431| `type` | string | `str` (default), `int`, `float`, `bool`, `list_str`, `list_dict`. Only int/float/bool are enforced at load; the rest are documentation. |
432| `enum` | list | Allowed values — enforced at load. Use it whenever the value set is closed. |
433| `required` | bool | Enforced at load: every requirement must carry the key non-null. |
434| `enum_display` / `enum_order` | dict / list | Per-enum-value visual metadata (label, abbreviation, color, icon) and ordering for PDF rendering. |
435| `chart_label` | string | Axis label when the attribute is used in charts. |
436| `output_formats` | object | `{"csv": bool, "ocsf": bool}`, both default `true` — toggles inclusion per output. |
437
438Key rules:
439
440- `--compliance` key = JSON basename without `.json` (`dora_2022_2554`).
441- Auto-discovered: no `__init__.py`, no formatter, no dispatcher registration.
442- `table_config.group_by`, `pdf_config.group_by_field` and every
443 `charts[].group_by` must reference a key declared in `attributes_metadata`.
444- Runtime type validation only covers `int`/`float`/`bool`; `str`/`list_str`/
445 `list_dict` are documentation-only.
446- Extending to a new provider = adding a key to `requirement.checks`. Nothing else.
447- **No automatic check-existence validation at load time** — a typo'd check id
448 silently produces a requirement with no findings. Always run the
449 check-existence cross-check (see Validation).
450- In universal files, always set `Provider` on every config constraint so a
451 guardrail authored for an AWS check never affects Azure/GCP scans of the
452 same requirement.
453
454## Legacy Schema Reference
455
456Base legacy file structure:
457
458```json
459{
460 "Framework": "FRAMEWORK_NAME",
461 "Name": "Full Framework Name with Version",
462 "Version": "X.X",
463 "Provider": "AWS",
464 "Description": "Framework description...",
465 "Requirements": [
466 {
467 "Id": "requirement_id",
468 "Name": "Optional requirement name",
469 "Description": "Requirement description",
470 "Attributes": [ ... ],
471 "Checks": ["check_name_1"],
472 "ConfigRequirements": [ ... ]
473 }
474 ]
475}
476```
477
478### Legacy fields, top level (`Compliance`)
479
480| Field | Type | Required | Notes |
481|---|---|---|---|
482| `Framework` | string | Yes (non-empty, validated) | Canonical identifier (`CIS`, `ENS`, `NIST-800-53-Revision-5`). |
483| `Name` | string | Yes (non-empty, validated) | Human-readable name with version. |
484| `Version` | string | Optional in the model — **never leave it empty in practice** | Empty Version silently degrades the `get_check_compliance()` key to `"{Framework}"` (gotcha #4). Must match the version substring in the filename. |
485| `Provider` | string | Yes (non-empty, validated) | Upper-cased single provider (`AWS`, `AZURE`, `GCP`, `M365`, ...). One file = one provider. |
486| `Description` | string | Yes | Framework scope and purpose. |
487| `Requirements` | array | Yes | Requirement objects (below), or `Mitre_Requirement` objects for MITRE files. |
488
489### Legacy fields, per requirement (`Compliance_Requirement`)
490
491| Field | Type | Required | Notes |
492|---|---|---|---|
493| `Id` | string | Yes | Unique within the framework; follow the source numbering exactly (`1.1`, `A.5.1`, `CCC.Core.CN01.AR01`). |
494| `Description` | string | Yes | Verbatim from the source catalog. |
495| `Name` | string | No | Optional short title (NIST-style catalogs use it). |
496| `Attributes` | array of objects | Yes | Parsed against the Union of attribute classes below; only `Attributes[0]` survives the universal adaptation and drives UI grouping. |
497| `Checks` | array of strings | Yes | Check ids automating the requirement; `[]` = manual. |
498| `ConfigRequirements` | array | No | Guardrails; `Provider` is omitted (the file is single-provider). |
499
500MITRE files use `Mitre_Requirement` instead, which adds `Tactics`,
501`SubTechniques`, `Platforms`, `TechniqueURL` at the requirement top level.
502
503### Attribute shapes per framework family
504
505Unlike universal (schema in-file), a legacy requirement's `Attributes` must
506match one of the Pydantic classes registered in
507`Compliance_Requirement.Attributes` — a shape matching no class **silently
508falls through to Generic**, dropping its specific fields. The most common
509shapes (full field sets in `compliance_models.py`):
510
511### CIS — `cis_{version}_{provider}`
512
513```json
514{
515 "Section": "1 Identity and Access Management",
516 "SubSection": "Optional subsection",
517 "Profile": "Level 1",
518 "AssessmentStatus": "Automated",
519 "Description": "...", "RationaleStatement": "...", "ImpactStatement": "...",
520 "RemediationProcedure": "...", "AuditProcedure": "...",
521 "AdditionalInformation": "...", "DefaultValue": "...", "References": "https://..."
522}
523```
524
525`Profile`: `Level 1|Level 2|E3 Level 1|E3 Level 2|E5 Level 1|E5 Level 2`.
526`AssessmentStatus`: `Automated|Manual`.
527
528### ENS — `ens_rd2022_{provider}`
529
530```json
531{
532 "IdGrupoControl": "op.acc.1", "Marco": "operacional",
533 "Categoria": "control de acceso", "DescripcionControl": "...",
534 "Nivel": "alto", "Tipo": "requisito",
535 "Dimensiones": ["trazabilidad", "autenticidad"],
536 "ModoEjecucion": "automatico", "Dependencias": []
537}
538```
539
540`Nivel`: `opcional|bajo|medio|alto`. `Tipo`: `refuerzo|requisito|recomendacion|medida`.
541`Dimensiones`: `confidencialidad|integridad|trazabilidad|autenticidad|disponibilidad`.
542
543### ISO 27001 — `iso27001_{year}_{provider}`
544
545```json
546{
547 "Category": "A.5 Organizational controls",
548 "Objetive_ID": "A.5.1", "Objetive_Name": "Policies for information security",
549 "Check_Summary": "Summary of what is being checked"
550}
551```
552
553Note: `Objetive_ID` / `Objetive_Name` use this exact (mis)spelling.
554
555### MITRE ATT&CK — `mitre_attack_{provider}` (separate requirement model)
556
557```json
558{
559 "Name": "Exploit Public-Facing Application", "Id": "T1190",
560 "Tactics": ["Initial Access"], "SubTechniques": [],
561 "Platforms": ["IaaS"], "Description": "...",
562 "TechniqueURL": "https://attack.mitre.org/techniques/T1190/",
563 "Checks": ["guardduty_is_enabled"],
564 "Attributes": [
565 {"AWSService": "Amazon GuardDuty", "Category": "Detect",
566 "Value": "Minimal", "Comment": "..."}
567 ]
568}
569```
570
571`AzureService`/`GCPService` for the other providers. `Category`:
572`Detect|Protect|Respond`. `Value`: `Minimal|Partial|Significant`.
573
574### CCC — `ccc_{provider}`
575
576```json
577{
578 "FamilyName": "Data", "FamilyDescription": "...",
579 "Section": "CCC.Core.CN01 Encrypt Data for Transmission", "SubSection": "",
580 "SubSectionObjective": "...",
581 "Applicability": ["tlp-green", "tlp-amber", "tlp-red"],
582 "Recommendation": "...",
583 "SectionThreatMappings": [{"ReferenceId": "CCC", "Identifiers": ["CCC.Core.TH02"]}],
584 "SectionGuidelineMappings": [{"ReferenceId": "NIST-CSF", "Identifiers": ["PR.DS-02"]}]
585}
586```
587
588`Applicability` holds TLP tags (`tlp-clear|tlp-green|tlp-amber|tlp-red`).
589
590### ASD Essential Eight — `asd_essential_eight_aws`
591
592```json
593{
594 "Section": "Patch applications", "MaturityLevel": "ML1",
595 "AssessmentStatus": "Automated", "CloudApplicability": "partial",
596 "MitigatedThreats": ["..."], "Description": "...",
597 "RationaleStatement": "...", "ImpactStatement": "...",
598 "RemediationProcedure": "...", "AuditProcedure": "...",
599 "AdditionalInformation": "...", "References": "..."
600}
601```
602
603`MaturityLevel`: `ML1|ML2|ML3`. `CloudApplicability`: `full|partial|limited|non-applicable`.
604
605### DISA STIG — `okta_idaas_stig_v1r2_okta`
606
607```json
608{
609 "Section": "...", "Severity": "high", "RuleID": "...", "StigID": "...",
610 "CCI": ["CCI-000015"], "CheckText": "...", "FixText": "..."
611}
612```
613
614`Severity`: `high|medium|low` (maps to CAT I/II/III).
615
616### Other registered shapes
617
618- **AWS Well-Architected** (`aws_well_architected_framework_{pillar}_pillar_aws`):
619 `Name`, `WellArchitectedQuestionId`, `WellArchitectedPracticeId`, `Section`,
620 `SubSection`, `LevelOfRisk`, `AssessmentMethod`, `Description`,
621 `ImplementationGuidanceUrl`.
622- **KISA ISMS-P** (`kisa_isms_p_2023_{provider}`): `Domain`, `Subdomain`,
623 `Section`, `AuditChecklist`, `RelatedRegulations`, `AuditEvidence`,
624 `NonComplianceCases`.
625- **C5** (`c5_{provider}`): `Section`, `SubSection`, `Type`, `AboutCriteria`,
626 `ComplementaryCriteria`.
627- **CSA CCM** (legacy shape; the shipped CSA CCM 4.0 is universal): `Section`,
628 `CCMLite`, `IaaS`, `PaaS`, `SaaS`, `ScopeApplicability`.
629- **Prowler ThreatScore** (`prowler_threatscore_{provider}`): `Title`,
630 `Section`, `SubSection`, `AttributeDescription`, `AdditionalInformation`,
631 `LevelOfRisk` (1–5), `Weight` (1/8/10/100/1000). Pillars: 1 IAM, 2 Attack
632 Surface, 3 Logging and Monitoring, 4 Encryption. Available for aws,
633 azure, gcp, kubernetes, m365, alibabacloud.
634- **Generic (fallback)**: `ItemId`, `Section`, `SubSection`, `SubGroup`,
635 `Service`, `Type`, `Comment` — all optional. Used by NIST, PCI, GDPR,
636 HIPAA, SOC2, FedRAMP, CISA, FFIEC, RBI, NIS2, GxP, SecNumCloud, etc.
637
638## Config Guardrails (`ConfigRequirements`)
639
640Requirements backed by [configurable checks](https://docs.prowler.com/developer-guide/configurable-checks)
641can be silently "satisfied" by a loosened `audit_config` (e.g. CIS demands
64245-day unused credentials but the scan ran with `max_unused_access_keys_days: 120`).
643Guardrails force such requirements to FAIL:
644
645```json
646"ConfigRequirements": [
647 {"Check": "iam_user_accesskey_unused",
648 "ConfigKey": "max_unused_access_keys_days", "Operator": "lte", "Value": 45}
649]
650```
651
652- Operators: `lte`/`gte` (numeric thresholds), `eq` (toggles/exact — use JSON
653 booleans, not 0/1), `in` (scalar in allowed set), `subset` (allowlists —
654 widening breaks it), `superset` (denylists — removing an entry breaks it).
655- `Value` must be the **strictest** setting the control text tolerates.
656- `ConfigKey` must be spelled exactly as the check reads it; unknown keys are
657 silently skipped (defaults assumed OK).
658- Guardrails only tighten (PASS→FAIL), never relax.
659- Universal files: lowercase `config_requirements` + mandatory `Provider` per
660 constraint.
661- Tests: `tests/lib/check/compliance_config_eval_test.py`,
662 `compliance_config_constraint_model_test.py`,
663 `compliance_config_requirements_data_test.py`, plus per-output tests under
664 `tests/lib/outputs/compliance/`.
665
666---
667
668## Workflow A: Sync a Framework With an Upstream Catalog
669
670Use when the framework is maintained upstream (CIS Benchmarks, FINOS CCC, CSA
671CCM, NIST, ENS, etc.) and Prowler needs to catch up.
672
673### Step 1 — Cache the upstream source
674
675Download every upstream file to a local cache so iterations don't hit the
676network. For FINOS CCC:
677
678```bash
679mkdir -p /tmp/ccc_upstream
680catalogs="core/ccc storage/object management/auditlog management/logging ..."
681for p in $catalogs; do
682 safe=$(echo "$p" | tr '/' '_')
683 gh api "repos/finos/common-cloud-controls/contents/catalogs/$p/controls.yaml" \
684 -H "Accept: application/vnd.github.raw" > "/tmp/ccc_upstream/${safe}.yaml"
685done
686```
687
688### Step 2 — Run the generic sync runner against a framework config
689
690The sync tooling is three layers, so adding a framework only takes a YAML
691config (plus a parser module for an unfamiliar upstream format):
692
693```text
694skills/prowler-compliance/assets/
695├── sync_framework.py # generic runner — works for any framework
696├── configs/ccc.yaml # per-framework config (canonical example)
697└── parsers/finos_ccc.py # parser module for FINOS CCC YAML
698```
699
700```bash
701python skills/prowler-compliance/assets/sync_framework.py \
702 skills/prowler-compliance/assets/configs/ccc.yaml
703```
704
705The runner loads the config, dynamically imports `parser.module`, calls
706`parse_upstream(config) -> list[dict]`, then applies generic post-processing
707(id-uniqueness safety net, `FamilyName` normalization, legacy check-mapping
708preservation with config-driven fallback keys) and writes the provider JSONs
709with Pydantic post-validation.
710
711**To add a new framework sync**:
712
7131. Write `assets/configs/{framework}.yaml` (see `ccc.yaml`). Required sections:
714 - `framework` — `name`, `display_name`, `version` (**never empty** — the
715 runner refuses to start, because empty Version breaks the
716 `get_check_compliance()` key), `description_template`.
717 - `providers` — list of `{key, display}` pairs.
718 - `output.path_template` — e.g.
719 `"prowler/compliance/{provider}/cis_{version}_{provider}.json"`.
720 - `upstream.dir` — local cache (Step 1).
721 - `parser.module` — module under `parsers/`; the rest of `parser.` is
722 passed through opaque.
723 - `post_processing.check_preservation.primary_key` (almost always `Id`) and
724 `fallback_keys` — lists of `Attributes[0]` field names composed into
725 tuples for recovering mappings when ids change. CCC:
726 `- [Section, Applicability]`; CIS: `- [Section, Profile]`; NIST:
727 `- [ItemId]`. List-valued fields are frozen to `frozenset` automatically.
728 - `post_processing.family_name_normalization` (optional) — raw → canonical
729 map; the UI groups by the exact attribute value, so upstream variants
730 otherwise become separate tree branches.
7312. Reuse an existing parser or write `parsers/{name}.py` implementing
732 `parse_upstream(config) -> list[dict]` returning Prowler-format
733 requirements with **guaranteed-unique ids**. The runner raises on
734 duplicates — it never silently renumbers, because mutating a canonical
735 upstream id (CIS `1.1.1`, NIST `AC-2(1)`) would be catastrophic. The parser
736 owns all upstream quirks: foreign-prefix rewriting, genuine collision
737 renumbering, multi-shape handling.
738
739**Gotchas the runner already handles** (from the FINOS CCC v2025.10 sync):
740
741- **Multiple upstream YAML shapes.** Most FINOS CCC catalogs use
742 `control-families: [...]` but `storage/object` uses top-level
743 `controls: [...]`. A single-shape parser silently drops entire catalogs —
744 this exact bug dropped ObjStor for a full iteration. Test with one file of
745 each shape.
746- **Whitespace collapse.** Upstream `|` block scalars keep newlines; Prowler
747 stores single-line. Collapse with `" ".join(value.split())`.
748- **Foreign-prefix id rewriting.** Upstream aliases requirements across
749 catalogs keeping the original prefix (`CCC.AuditLog.CN08.AR01` nested under
750 `CCC.Logging.CN03`) — rewrite to fit the parent (`CCC.Logging.CN03.AR01`).
751- **Genuine upstream collisions.** Two different requirements sharing one id
752 (upstream typo): renumber the second to the next free number; check-mapping
753 preservation recovers by the fallback keys.
754- **Populate `Version`** — fail-fast beats the silent broken-key bug.
755
756### Step 3 — Validate before committing
757
758Run the full Validation section below (universal loader + check existence +
759CLI smoke + pytest).
760
761### Step 4 — Add an attribute model if needed
762
763Only if the framework has fields beyond
764`Generic_Compliance_Requirement_Attribute` and must stay legacy. Add the class
765to `compliance_models.py` and register it in the
766`Compliance_Requirement.Attributes` Union **before Generic** (Generic stays
767last). For new frameworks, prefer universal `attributes_metadata` instead.
768
769---
770
771## Workflow B: Audit Check Mappings as a Cloud Auditor
772
773Use when the user asks to review existing mappings. This is the
774highest-value compliance task — it surfaces padded mappings with zero actual
775coverage and missing mappings for legitimate coverage.
776
777### The golden rule
778
779> A Prowler check's title/risk MUST **literally describe what the requirement
780> text says**. "Related" is not enough. If no check actually addresses the
781> requirement, leave the checks list empty (MANUAL) — **honest MANUAL is worth
782> more than padded coverage**.
783
784### Audit process
785
7861. **Build a per-provider check inventory** — `assets/build_inventory.py`
787 (writes `/tmp/checks_{provider}.json` for every provider discovered under
788 `prowler/providers/`).
7892. **Query it** — `assets/query_checks.py` (run from the repository root):
790
791 ```bash
792 python skills/prowler-compliance/assets/query_checks.py aws encryption transit # keyword AND-search
793 python skills/prowler-compliance/assets/query_checks.py aws --service iam # all iam checks
794 python skills/prowler-compliance/assets/query_checks.py aws --id kms_cmk_rotation_enabled
795 ```
796
7973. **Dump a framework section with current mappings** — `assets/dump_section.py`:
798
799 ```bash
800 python skills/prowler-compliance/assets/dump_section.py ccc "CCC.Core."
801 python skills/prowler-compliance/assets/dump_section.py cis_5.0_aws "1."
802 ```
803
8044. **Encode explicit REPLACE decisions** — `assets/audit_framework_template.py`:
805
806 ```python
807 DECISIONS = {}
808 DECISIONS["CCC.Core.CN01.AR01"] = {
809 "aws": ["cloudfront_distributions_https_enabled", ...],
810 "azure": ["storage_secure_transfer_required_is_enabled", ...],
811 "gcp": ["cloudsql_instance_ssl_connections"],
812 # Missing provider key = leave the legacy mapping untouched
813 }
814 # Empty list = EXPLICITLY MANUAL (overwrites legacy)
815 DECISIONS["CCC.Core.CN01.AR07"] = {"aws": [], "azure": [], "gcp": []}
816 ```
817
818 **REPLACE, not PATCH.** Full lists make the audit reproducible and surface
819 hidden assumptions in the legacy data.
8205. **Pre-validate** every check id against the inventory; the script MUST
821 abort with stderr listing typos (real audits caught
822 `storage_secure_transfer_required_enabled` →
823 `storage_secure_transfer_required_is_enabled`,
824 `sqlserver_minimum_tls_version_12` →
825 `sqlserver_recommended_minimal_tls_version`, and several checks that
826 simply don't exist).
8276. **Apply + validate + test**:
828
829 ```bash
830 python /path/to/audit_script.py
831 uv run pytest -n auto tests/lib/outputs/compliance/ tests/lib/check/ -q
832 ```
833
834For the curated mapping table (requirement text → AWS/Azure/GCP checks) and
835the list of controls Prowler genuinely cannot verify, see
836[references/check-mapping-reference.md](references/check-mapping-reference.md).
837
838---
839
840## Workflow C: Add a New Universal Framework
841
8421. Author `prowler/compliance/{framework}_{version}.json` following the
843 Universal Schema Reference above (use `dora_2022_2554.json` or
844 `csa_ccm_4.0.json` as template).
8452. Declare every attribute in `attributes_metadata` (with `required`/`enum`
846 where possible — that's your load-time validation) and a
847 `outputs.table_config.group_by`.
8483. Map checks per provide
849
850…(truncated)