diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 2c0d00c499..5730cc4a1e 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler SDK** are documented in this file. +## [5.32.1] (Prowler UNRELEASED) + +### 🐞 Fixed + +- `KeyError: 'MANUAL'` crash while rendering the compliance summary table (e.g. CIS Microsoft 365) when a framework has manual, checks-less requirements with a Level 1/Level 2 profile; `MANUAL` findings are now skipped in the PASS/FAIL section tally instead of raising [(#11822)](https://github.com/prowler-cloud/prowler/issues/11822) + +--- + ## [5.32.0] (Prowler v5.32.0) ### 🚀 Added diff --git a/prowler/lib/check/compliance_config_eval.py b/prowler/lib/check/compliance_config_eval.py index 763e1389c2..0024d41e3f 100644 --- a/prowler/lib/check/compliance_config_eval.py +++ b/prowler/lib/check/compliance_config_eval.py @@ -395,6 +395,12 @@ def accumulate_group_status( ) -> None: """Count a finding once per group, upgrading a counted PASS to FAIL on conflict (mutates ``counts``/``seen``).""" previous = seen.get(index) + if status == "MANUAL": + # MANUAL findings come from manual, checks-less requirements and are + # informational only: they have no PASS/FAIL/Muted column in the section + # tally, so counting them would raise KeyError on counts[status] += 1. + # Skip them (an unexpected status still raises loudly below). + return if previous is None: seen[index] = status counts[status] += 1 diff --git a/tests/lib/check/compliance_config_eval_test.py b/tests/lib/check/compliance_config_eval_test.py index 4acec9bb4c..74537344c4 100644 --- a/tests/lib/check/compliance_config_eval_test.py +++ b/tests/lib/check/compliance_config_eval_test.py @@ -351,6 +351,37 @@ class Test_accumulate_group_status: with pytest.raises(KeyError): accumulate_group_status(0, "Muted", counts, {}) + def test_manual_status_is_ignored_not_counted(self): + # A MANUAL finding (from a manual, checks-less requirement) has no + # PASS/FAIL/Muted column: it must be skipped, not raise KeyError, and + # not appear in the tally. Regression test for the M365 CIS compliance + # crash "KeyError: 'MANUAL'" (issue #11822). + counts = {"FAIL": 0, "PASS": 0} + seen = {} + accumulate_group_status(0, "MANUAL", counts, seen) + assert counts == {"FAIL": 0, "PASS": 0} + assert seen == {} + + def test_manual_mixed_with_pass_and_fail(self): + # MANUAL findings interleaved with real PASS/FAIL ones only skip + # themselves; the PASS/FAIL tally is unaffected. + counts = {"FAIL": 0, "PASS": 0} + seen = {} + accumulate_group_status(0, "MANUAL", counts, seen) + accumulate_group_status(1, "PASS", counts, seen) + accumulate_group_status(2, "FAIL", counts, seen) + accumulate_group_status(3, "MANUAL", counts, seen) + assert counts == {"FAIL": 1, "PASS": 1} + + def test_manual_ignored_on_counts_with_muted_key(self): + # MANUAL is skipped regardless of the counts shape (e.g. the universal + # table's PASS/FAIL/Muted buckets), never creating a "MANUAL" key. + counts = {"FAIL": 0, "PASS": 0, "Muted": 0} + seen = {} + accumulate_group_status(0, "MANUAL", counts, seen) + assert counts == {"FAIL": 0, "PASS": 0, "Muted": 0} + assert "MANUAL" not in counts + class Test_apply_config_status: def test_none_config_status_keeps_finding(self):