From 2304bf0093aec09019ac5714f93d9de03755a9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Thu, 23 Apr 2026 13:28:30 +0200 Subject: [PATCH 1/2] feat(compliance): add CIS pdf reporting (#10650) --- api/CHANGELOG.md | 6 +- api/src/backend/api/tests/test_views.py | 45 ++ api/src/backend/api/v1/views.py | 63 ++ api/src/backend/tasks/assets/img/cis_logo.png | Bin 0 -> 134333 bytes api/src/backend/tasks/jobs/report.py | 233 +++++- .../backend/tasks/jobs/reports/__init__.py | 11 +- api/src/backend/tasks/jobs/reports/cis.py | 755 ++++++++++++++++++ .../backend/tasks/jobs/reports/components.py | 46 ++ api/src/backend/tasks/jobs/reports/config.py | 28 + api/src/backend/tasks/tasks.py | 7 +- api/src/backend/tasks/tests/test_reports.py | 266 +++++- .../backend/tasks/tests/test_reports_cis.py | 532 ++++++++++++ ui/CHANGELOG.md | 4 + ui/actions/scans/scans.ts | 125 ++- .../compliance/[compliancetitle]/page.tsx | 31 +- ui/app/(prowler)/compliance/page.tsx | 12 + ui/components/compliance/compliance-card.tsx | 16 +- .../compliance-download-container.test.tsx | 13 +- .../compliance-download-container.tsx | 7 +- .../compliance/compliance-overview-grid.tsx | 8 + .../compliance-report-types.test.ts | 156 ++++ ui/lib/compliance/compliance-report-types.ts | 124 ++- ui/lib/helper.ts | 29 +- 23 files changed, 2412 insertions(+), 105 deletions(-) create mode 100644 api/src/backend/tasks/assets/img/cis_logo.png create mode 100644 api/src/backend/tasks/jobs/reports/cis.py create mode 100644 api/src/backend/tasks/tests/test_reports_cis.py create mode 100644 ui/lib/compliance/compliance-report-types.test.ts diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 2d7342a293..0b3a9ffff8 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the **Prowler API** are documented in this file. ## [1.26.0] (Prowler UNRELEASED) +### 🚀 Added + +- CIS Benchmark PDF report generation for scans, exposing the latest CIS version per provider via `GET /scans/{id}/cis/{name}/` and picking the variant dynamically via `_pick_latest_cis_variant` (no hard-coded provider → version mapping) [(#10650)](https://github.com/prowler-cloud/prowler/pull/10650) + ### 🔄 Changed - Allows tenant owners to expel users from their organizations [(#10787)](https://github.com/prowler-cloud/prowler/pull/10787) @@ -736,4 +740,4 @@ All notable changes to the **Prowler API** are documented in this file. - Findings metadata endpoint received a performance improvement [(#6863)](https://github.com/prowler-cloud/prowler/pull/6863) - Increased the allowed length of the provider UID for Kubernetes providers [(#6869)](https://github.com/prowler-cloud/prowler/pull/6869) ---- +--- \ No newline at end of file diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index b81ae0c677..736cbbd56e 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -4113,6 +4113,51 @@ class TestScanViewSet: assert cd.startswith('attachment; filename="') assert cd.endswith(f'filename="{fname.name}"') + def test_cis_no_output(self, authenticated_client, scans_fixture): + """CIS PDF endpoint must 404 when the scan has no output_location.""" + scan = scans_fixture[0] + scan.state = StateChoices.COMPLETED + scan.output_location = "" + scan.save() + + url = reverse("scan-cis", kwargs={"pk": scan.id}) + resp = authenticated_client.get(url) + assert resp.status_code == status.HTTP_404_NOT_FOUND + assert ( + resp.json()["errors"]["detail"] + == "The scan has no reports, or the CIS report generation task has not started yet." + ) + + def test_cis_local_file(self, authenticated_client, scans_fixture, monkeypatch): + """CIS PDF endpoint must serve the latest generated PDF.""" + scan = scans_fixture[0] + scan.state = StateChoices.COMPLETED + + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + base = tmp_path / "reports" + cis_dir = base / "cis" + cis_dir.mkdir(parents=True, exist_ok=True) + fname = cis_dir / "prowler-output-aws-20260101000000_cis_report.pdf" + fname.write_bytes(b"%PDF-1.4 fake pdf") + + scan.output_location = str(base / "scan.zip") + scan.save() + + monkeypatch.setattr( + glob, + "glob", + lambda p: [str(fname)] if p.endswith("*_cis_report.pdf") else [], + ) + + url = reverse("scan-cis", kwargs={"pk": scan.id}) + resp = authenticated_client.get(url) + assert resp.status_code == status.HTTP_200_OK + assert resp["Content-Type"] == "application/pdf" + cd = resp["Content-Disposition"] + assert cd.startswith('attachment; filename="') + assert cd.endswith(f'filename="{fname.name}"') + @patch("api.v1.views.Task.objects.get") @patch("api.v1.views.TaskSerializer") def test__get_task_status_returns_none_if_task_not_executing( diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 28b5fd49f0..99813576f5 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1926,6 +1926,27 @@ class ProviderViewSet(DisablePaginationMixin, BaseRLSViewSet): ), }, ), + cis=extend_schema( + tags=["Scan"], + summary="Retrieve CIS Benchmark compliance report", + description="Download the CIS Benchmark compliance report as a PDF file. " + "When a provider ships multiple CIS versions, the report is generated " + "for the highest available version.", + request=None, + responses={ + 200: OpenApiResponse( + description="PDF file containing the CIS compliance report" + ), + 202: OpenApiResponse(description="The task is in progress"), + 401: OpenApiResponse( + description="API key missing or user not Authenticated" + ), + 403: OpenApiResponse(description="There is a problem with credentials"), + 404: OpenApiResponse( + description="The scan has no CIS reports, or the CIS report generation task has not started yet" + ), + }, + ), ) @method_decorator(CACHE_DECORATOR, name="list") @method_decorator(CACHE_DECORATOR, name="retrieve") @@ -1994,6 +2015,9 @@ class ScanViewSet(BaseRLSViewSet): elif self.action == "csa": if hasattr(self, "response_serializer_class"): return self.response_serializer_class + elif self.action == "cis": + if hasattr(self, "response_serializer_class"): + return self.response_serializer_class return super().get_serializer_class() def partial_update(self, request, *args, **kwargs): @@ -2236,6 +2260,45 @@ class ScanViewSet(BaseRLSViewSet): content, filename = loader return self._serve_file(content, filename, "text/csv") + @action( + detail=True, + methods=["get"], + url_name="cis", + ) + def cis(self, request, pk=None): + scan = self.get_object() + running_resp = self._get_task_status(scan) + if running_resp: + return running_resp + + if not scan.output_location: + return Response( + { + "detail": "The scan has no reports, or the CIS report generation task has not started yet." + }, + status=status.HTTP_404_NOT_FOUND, + ) + + if scan.output_location.startswith("s3://"): + 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), + "cis", + "*_cis_report.pdf", + ) + loader = self._load_file(prefix, s3=True, bucket=bucket, list_objects=True) + else: + base = os.path.dirname(scan.output_location) + pattern = os.path.join(base, "cis", "*_cis_report.pdf") + loader = self._load_file(pattern, s3=False) + + if isinstance(loader, Response): + return loader + + content, filename = loader + return self._serve_file(content, filename, "application/pdf") + @action( detail=True, methods=["get"], diff --git a/api/src/backend/tasks/assets/img/cis_logo.png b/api/src/backend/tasks/assets/img/cis_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7c1568da5ec877a9ca7d9238c153a2be92d4f8ae GIT binary patch literal 134333 zcma&Oc{r5O-#AK81tQ;=c&)<_uuba7x#6|JuNwC{^uc(&}R_H7VD$X1qj6Z5(GlFfsJH(hol^mf8;D~ulMI`XliP194FSVXz%}nI&@6)@Okf(4LwvVurCMJ}GZm0mHwm;geb+360XbN; z5-A>oM>KxI|A1?>+^3gfUZ;KTXkBM|cYA2eyVk~G7TuDl$0qwPcVi=iMZ|vY`3GR| z(4X~(m2J2R@bf0uTx>@@vM|Y9h-8xK+)6(45Me#hv?ibvzv=^+o0|B1ZND;q5$t*0 z&}tVSL-B`+lUfR#^C!CTpYyg_m4=vEOt%3hMJ#==ay{{+5u7L|URI9KQ8xJO*)4>O zitlt=U>;p(_IGnhH$w-yrP65#^BW&&t*)jPJowFYa`+nFPXe(5`m% zSy7~=Fksc2jvq%7f0$gCdELKc$H~T^2%nUpTa3Fhp6y{;E#0#jCG|F@u$_;-jT5IHyGohEk10cX$PmN%>UueozJ`7e0;RfH zE&Tf4zw0PHxXs27IaNqDv?9=CK%OlIIaAZXv~cY~21?g^deGy2G~kjp*qrEedo^o9 zC-RES!aP?&*siyHBae#MWj0KNT{Jz`v2kJuNp z7jA(B^3ujU5|}QECnuf+EE!g^hVXfVB6o>3-)G0|3QkFMZ~jN%1}^aOr&>6m~hn#(}&MQB=U$0owi(zSk07PLtiiRUu~HmVRp4~<)*`At>t8<^P` z*DGC}Qe(ytnQ`$29MtM&=gt?J^WG9b6+tD$#uPVVJ4WZYqXO;RA^w)hSu?o9S`Ld!n}+ zA3*=JiP#q`UJku|m&2esUhXU2=`krZ4`@tzSl^xiJk=K)xrU^?0#6)#BnV# zPsCLmy66%@V){zJ88GDxGP$R`$B@nDs<*#mMHGgT0F=58FQcu1#+y;O^xDDLB>0$# zPsmq~Nuu<$Z}o{dukC5CR`Fv_Wf5_53VaCINEp|Ye1iY3UBM-)9FocDKXpSvn@C}3 z@7$@9%enL)ou`}y;erh6$M2DVy?u7KxV3@q2d0UKezgcgCKqVij@ts}Qt?(p4u@An z?~3em%+G4W8-=OnvLiv1>2Cg>tXA};^=To1Ll4P?)|S$Pr`j&t)z82|SeG$>;XtAD z3rr+5@vd~d+30zbjCN(kGmGa`(~C>?fM(4tvX&Rt8|Df<4JCq3$`$@}GG|Pisv~d{%^7$WQm|slFn=bc$VswEEntmBQkcyM`%epmV}b7Vf;s{ZF45GMF-1$O{wWxC4N#*pr6)E3p| z5eNL7uGJt~&lia1RPeVAm%ca4dWdrx^|}fIo5t5d?lFts8~HYeTpJvC9VGSep{I8N zdFL_461G&0o^QO*E%J8u5dB!B#Khphvo9lRTnUxuOXMcWmE~$fjp{PTixv zw@>~3PYOd(nJjV%7L376yi;ss?Il1s+P*^77rY^3Tr(-wJvoAJD`@xR5HhZAG%kb^ z=U&#B6=c$&`jD%1O)x#C+H9cL0^kT(Qj;7h9RFw@rU4-Z?EQh>zv)ilq!c}*Vc|e&AQ5%Z_ji!e?EWLl({h)Cr%P^e zn2d_AIX}uIHIP4zXoWpXH8gP@{_Ih?iWo0as(2MH=xhx3+jR;6t;-Jd!FNFatMVPa zu!k_o0GXcQ`NGp=^M7a|1a%@i1u`%*TsBiwA6_=Dt@?a<&5TESCaiT3?0Sjdh@bl6`*m^T6DyZj=Ki~lZgdCV27OEwTA z|5(C|dcDNR1@&DUo_~~004P%~^GZK7Y83sO^yJ zpJum+{au~EYxwwMjv|6Nto=A@xWaKU4j_FeH-z%}WL%EwA9yZXYgEHhSnm-a_h!wB z-DFaOsHy7&hsT3 zr?&&mTMivZPxqXfgqGSY4WmoCwl^by%GSyqebv3Y89cIAB`Z@1)A#~YWMYH}a(l`zy#MOI70FNcARy1<#<)8P_f_p(+@F)g zxT~uxQF!MP5f0DVNO#}d?vxB+UxfsTO2@Pk^)NnWVFr0R1~DBlPK${^h786&WV#gP z7oO!43g5d|Ft!2ah;e-We&Au$i!+nbe@ycKjf@+AY=TMAWUR??4P>L4Q~i)0ur2Ra zKY0Ui33+kp=`lGaX)oBk$wlITO-RrK`e-T;7hoD(x;R(tZqsY6W1EuO@q)%1di%@> z)d7n8J1>Oy&>!->w4v0!xjO9;3(ed3qHVNYa7M2+3b}eM%3a2_g?oJ)`wH){ZkU+v zC`)|=x}mk#m!|;XAQxFZcniN}Rz3z@!lk6l@o!Sdsds{8wqS_*F`Vb!8hpuC;dr`z z{qjeEDfiW*5s7TP|B5wn@MvcEg>IUFq9eU)6Nu|FX=(mIlTol_E)^_JN3KU-DF2V^ z0LeMR5?3kz%RFf5&aD2uph8W#yn7yr{|wS|A1A}_TaL;*4UTX6)P#dS7GB1t(u3KNu$dOQRrPx z;QeN${0|JMpjEl_J76H@jAqtBxKTqiNfKug{3wn@u`}A8_~OxyYiKX9_f=Pu_|NGn zp|T-dN@F_hEbkPTwN*HlVpIPV`hv&OUe*R*#=O$81Wr!wWn9mGu4|9Z?Ajz^0Lvey zEn6w17(K*1PmG7+if_&vmH(dcLA*KB7@~Bksm~y|J;BhAKsNvA#n*?D3@-42@C0u*bi0(WIV$+hduX46V1{@LJ(?-w_rrN{2_{s-EL|^(M^NmCN>l9;gkxZSO>7+BJ0l%zM6Ns z*viScPW~i$Fs|Dd@kgO7IDv_lRXdJZq;KtCRzfq`|49x6QXq%2VpEr8VZ*GCF7tp4 z>8hE9Mg%8!ZBO9mWS1Nf4!v7BnS0TG=(-rv4-6n#>O>#O1wx8vih5v4+#7R8gKu1@ z`!8NR6}%A2amro1u6wu@M$jUVfqyC2CU5@>G~5i;9fu6);}?kjVb zxH^`icW+Sy2$^!A7Xi!!31mpa0KTNySC!aa{_?-c40)o;;=&x33n8?pJcnG}t8Y}@ z#Si2c+;l3IM0&a&^rt&HR0mk0B`?Yj(Le?wT}QOIcFw48CGWuYt)$BwhX0`|B!`D3 zaUW8Nv)$Ykh9=%c6LY}0V?lFVZOw%DOH8HCl(TuefvZ@xzWABI^o!UJw73vtdQ%T= z2=2`gI6NnG^3Cff|HXn3T|*y+9UJ6!n4$4jtyy75@k=;DM#u(>zgx9oJdIx#np)a< zQmlWW$s_DxS$U5{Q_?t*_6iLgl_}rZ5C^lGSSP2QnIN%HwEw?|8yc_1iGX)6_GGc;wSyvotYynXRiUeTNUs=QQ1|iowt^Q`|{nQH#$1 zO^5ettkW~mV>w_Mh6d|nBj(6M*T2$o41nYm+jn0Kjuxrv7hnfsy1qlp%s3@+dNXTS zp#Ov`{li|cQ1EVrje=Y7>>g=o*}ySB)!1t$fm8o*hJO5GDRgcfGttIN|CoAyA6(5d zam8hFc!q4+&by|=?sDs(%ox=;Qe~}?=EMlCUqh;0=(X*Awtc#H-f(4KtNHZRys!i8 zu|f7)flJo_S;-5W;_I@Y_Yk89qz*H$N~bqVTf%PIw&1oK4Z+J`);Bq@+Z9-GDp)#_ zi=rivgYUJLBd~_dkR1K|7JO8EgyU|lcq@U?GXh_VUl2{A9{)%CQ+_P{XmrmxOpwB~ z#(sMUs`^$rPk%{vXIX)Pn0t2-!D@p0lf-AQdzZA@fZ}Tx)d#=nY znFqhwHMB<8pcXNU0iveq{Utal%b>EW{X^gDRSKiM6N>7Jh66!KgKK&xofZYR5^fYQ zyC}@Yb*5;yhPNEjxAT;Fa6GA_)O&M#ktu-`VA>gH&`VHb_#gKexF>xW@gF9?(nyXP zhA62yL#f+=ZvV&wS!Yom(ps*XTA~6&=Q}n}(0IMy%HB)O+`{-*nwC2AuYISTRI^<- z)S8~49e_PoDC{U{ReX7=^*#)t|11=xeqRI)pfsd~Tt>-oFi)Jz9M(=#RGJiq?*HNNfoFN>crWzJzV$kg8V+Tf(XRc

R%z)#1 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Ð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\d+(?:\.\d+)+)_(?P.+)$") + + +def _pick_latest_cis_variant(compliance_ids: Iterable[str]) -> str | None: + """Return the CIS compliance_id with the highest semantic version. + + CIS ships many variants per provider (e.g. cis_1.4_aws, ..., cis_6.0_aws). + A lexicographic sort is incorrect for version strings like ``1.10`` vs + ``1.2``; this helper parses the version into a tuple of ints so ``1.10`` + is correctly ordered after ``1.2``. Malformed names are skipped so a + broken JSON cannot crash the whole CIS pipeline. + + Args: + compliance_ids: Iterable of CIS compliance identifiers. Expected to + belong to a single provider (callers should pass the already + filtered keys from ``Compliance.get_bulk(provider_type)``). + + Returns: + The compliance_id with the highest parsed version, or ``None`` if no + well-formed CIS identifier was found. + """ + best_key: tuple[int, ...] | None = None + best_name: str | None = None + for name in compliance_ids: + match = _CIS_VARIANT_RE.match(name) + if not match: + continue + try: + key = tuple(int(part) for part in match.group("version").split(".")) + except ValueError: + # Defensive: the regex already guarantees numeric chunks, but we + # keep the guard so a future regex change cannot crash callers. + continue + if best_key is None or key > best_key: + best_key = key + best_name = name + return best_name + def generate_threatscore_report( tenant_id: str, @@ -191,6 +238,53 @@ def generate_csa_report( ) +def generate_cis_report( + tenant_id: str, + scan_id: str, + compliance_id: str, + output_path: str, + provider_id: str, + only_failed: bool = True, + include_manual: bool = False, + provider_obj: Provider | None = None, + requirement_statistics: dict[str, dict[str, int]] | None = None, + findings_cache: dict[str, list[FindingOutput]] | None = None, +) -> None: + """ + Generate a PDF compliance report for a specific CIS Benchmark variant. + + Unlike single-version frameworks (ENS, NIS2, CSA), CIS has multiple + variants per provider (e.g., cis_1.4_aws, cis_5.0_aws, cis_6.0_aws). This + wrapper is called once per variant, receiving the specific compliance_id. + + 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 specific CIS variant (e.g., "cis_5.0_aws"). + output_path: Output PDF file path. + provider_id: Provider ID for the scan. + only_failed: If True, only include failed requirements in detailed section. + include_manual: If True, include manual requirements in detailed section. + provider_obj: Pre-fetched Provider object to avoid duplicate queries. + requirement_statistics: Pre-aggregated requirement statistics. + findings_cache: Cache of already loaded findings to avoid duplicate queries. + """ + generator = CISReportGenerator(FRAMEWORK_REGISTRY["cis"]) + + generator.generate( + tenant_id=tenant_id, + scan_id=scan_id, + compliance_id=compliance_id, + output_path=output_path, + provider_id=provider_id, + provider_obj=provider_obj, + requirement_statistics=requirement_statistics, + findings_cache=findings_cache, + only_failed=only_failed, + include_manual=include_manual, + ) + + def generate_compliance_reports( tenant_id: str, scan_id: str, @@ -199,6 +293,7 @@ def generate_compliance_reports( generate_ens: bool = True, generate_nis2: bool = True, generate_csa: bool = True, + generate_cis: bool = True, only_failed_threatscore: bool = True, min_risk_level_threatscore: int = 4, include_manual_ens: bool = True, @@ -206,6 +301,8 @@ def generate_compliance_reports( only_failed_nis2: bool = True, only_failed_csa: bool = True, include_manual_csa: bool = False, + only_failed_cis: bool = True, + include_manual_cis: bool = False, ) -> dict[str, dict[str, bool | str]]: """ Generate multiple compliance reports with shared database queries. @@ -215,6 +312,13 @@ def generate_compliance_reports( - Aggregating requirement statistics once (shared across all reports) - Reusing compliance framework data when possible + For CIS a single PDF is produced per run: the one matching the highest + available CIS version for the scan's provider (picked dynamically from + ``Compliance.get_bulk`` via :func:`_pick_latest_cis_variant`). The + returned ``results["cis"]`` entry has the same flat shape as the other + single-version frameworks — the picked variant is an internal detail, + not surfaced in the result. + Args: tenant_id: The tenant ID for Row-Level Security context. scan_id: The ID of the scan to generate reports for. @@ -223,6 +327,8 @@ def generate_compliance_reports( generate_ens: Whether to generate ENS report. generate_nis2: Whether to generate NIS2 report. generate_csa: Whether to generate CSA CCM report. + generate_cis: Whether to generate a CIS Benchmark report for the + latest CIS version available for the provider. only_failed_threatscore: For ThreatScore, only include failed requirements. min_risk_level_threatscore: Minimum risk level for ThreatScore critical requirements. include_manual_ens: For ENS, include manual requirements. @@ -230,22 +336,26 @@ def generate_compliance_reports( only_failed_nis2: For NIS2, only include failed requirements. only_failed_csa: For CSA CCM, only include failed requirements. include_manual_csa: For CSA CCM, include manual requirements. + only_failed_cis: For CIS, only include failed requirements in detailed section. + include_manual_cis: For CIS, include manual requirements in detailed section. Returns: - Dictionary with results for each report type. + Dictionary with results for each report type. Every value has the + same flat shape: ``{"upload": bool, "path": str, "error"?: str}``. """ logger.info( "Generating compliance reports for scan %s with provider %s" - " (ThreatScore: %s, ENS: %s, NIS2: %s, CSA: %s)", + " (ThreatScore: %s, ENS: %s, NIS2: %s, CSA: %s, CIS: %s)", scan_id, provider_id, generate_threatscore, generate_ens, generate_nis2, generate_csa, + generate_cis, ) - results = {} + results: dict = {} # Validate that the scan has findings and get provider info with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS): @@ -259,6 +369,8 @@ def generate_compliance_reports( results["nis2"] = {"upload": False, "path": ""} if generate_csa: results["csa"] = {"upload": False, "path": ""} + if generate_cis: + results["cis"] = {"upload": False, "path": ""} return results provider_obj = Provider.objects.get(id=provider_id) @@ -299,11 +411,18 @@ def generate_compliance_reports( results["csa"] = {"upload": False, "path": ""} generate_csa = False + # For CIS we do NOT pre-check the provider against a hard-coded whitelist + # (that list drifts the moment a new CIS JSON ships). Instead, we let + # `_pick_latest_cis_variant` over `Compliance.get_bulk(provider_type)` + # return None for providers that lack CIS, and treat that as "nothing to + # do" below. + if ( not generate_threatscore and not generate_ens and not generate_nis2 and not generate_csa + and not generate_cis ): return results @@ -350,6 +469,13 @@ def generate_compliance_reports( scan_id, compliance_framework="csa", ) + cis_path = _generate_compliance_output_directory( + DJANGO_TMP_OUTPUT_DIRECTORY, + provider_uid, + tenant_id, + scan_id, + compliance_framework="cis", + ) out_dir = str(Path(threatscore_path).parent.parent) except Exception as e: logger.error("Error generating output directory: %s", e) @@ -362,6 +488,8 @@ def generate_compliance_reports( results["nis2"] = error_dict.copy() if generate_csa: results["csa"] = error_dict.copy() + if generate_cis: + results["cis"] = error_dict.copy() return results # Generate ThreatScore report @@ -569,12 +697,92 @@ def generate_compliance_reports( logger.error("Error generating CSA CCM report: %s", e) results["csa"] = {"upload": False, "path": "", "error": str(e)} - # Clean up temporary files if all reports were uploaded successfully - all_uploaded = all( - result.get("upload", False) - for result in results.values() - if result.get("upload") is not None - ) + # Generate CIS Benchmark report for the latest available version only. + # CIS ships multiple versions per provider (e.g. cis_1.4_aws, cis_5.0_aws, + # cis_6.0_aws); we dynamically pick the highest semantic version at run + # time rather than hard-coding a per-provider mapping. `Compliance.get_bulk` + # is the single source of truth for which providers have CIS. + if generate_cis: + latest_cis: str | None = None + try: + frameworks_bulk = Compliance.get_bulk(provider_type) + latest_cis = _pick_latest_cis_variant( + name for name in frameworks_bulk.keys() if name.startswith("cis_") + ) + except Exception as e: + logger.error("Error discovering CIS variants for %s: %s", provider_type, e) + results["cis"] = {"upload": False, "path": "", "error": str(e)} + + if "cis" not in results: + if latest_cis is None: + logger.info("No CIS variants available for provider %s", provider_type) + results["cis"] = {"upload": False, "path": ""} + else: + logger.info( + "Selected latest CIS variant for provider %s: %s", + provider_type, + latest_cis, + ) + pdf_path_cis = f"{cis_path}_cis_report.pdf" + try: + generate_cis_report( + tenant_id=tenant_id, + scan_id=scan_id, + compliance_id=latest_cis, + output_path=pdf_path_cis, + provider_id=provider_id, + only_failed=only_failed_cis, + include_manual=include_manual_cis, + provider_obj=provider_obj, + requirement_statistics=requirement_statistics, + findings_cache=findings_cache, + ) + + upload_uri_cis = _upload_to_s3( + tenant_id, + scan_id, + pdf_path_cis, + f"cis/{Path(pdf_path_cis).name}", + ) + + if upload_uri_cis: + results["cis"] = { + "upload": True, + "path": upload_uri_cis, + } + logger.info( + "CIS report %s uploaded to %s", + latest_cis, + upload_uri_cis, + ) + else: + results["cis"] = {"upload": False, "path": out_dir} + logger.warning( + "CIS report %s saved locally at %s", + latest_cis, + out_dir, + ) + + except Exception as e: + logger.error("Error generating CIS report %s: %s", latest_cis, e) + results["cis"] = { + "upload": False, + "path": "", + "error": str(e), + } + finally: + # Free ReportLab/matplotlib memory before moving on. + gc.collect() + + # Clean up temporary files only if every requested report has been + # successfully uploaded. All result entries now share the same flat + # shape, so the check is a single comprehension. + upload_flags = [ + bool(entry.get("upload", False)) + for entry in results.values() + if isinstance(entry, dict) and entry.get("upload") is not None + ] + all_uploaded = bool(upload_flags) and all(upload_flags) if all_uploaded: try: @@ -595,6 +803,7 @@ def generate_compliance_reports_job( generate_ens: bool = True, generate_nis2: bool = True, generate_csa: bool = True, + generate_cis: bool = True, ) -> dict[str, dict[str, bool | str]]: """ Celery task wrapper for generate_compliance_reports. @@ -607,9 +816,12 @@ def generate_compliance_reports_job( generate_ens: Whether to generate ENS report. generate_nis2: Whether to generate NIS2 report. generate_csa: Whether to generate CSA CCM report. + generate_cis: Whether to generate the CIS Benchmark report for the + latest CIS version available for the provider. Returns: - Dictionary with results for each report type. + Dictionary with results for each report type. Every entry shares the + same flat ``{"upload", "path", "error"?}`` shape. """ return generate_compliance_reports( tenant_id=tenant_id, @@ -619,4 +831,5 @@ def generate_compliance_reports_job( generate_ens=generate_ens, generate_nis2=generate_nis2, generate_csa=generate_csa, + generate_cis=generate_cis, ) diff --git a/api/src/backend/tasks/jobs/reports/__init__.py b/api/src/backend/tasks/jobs/reports/__init__.py index 1fc475a467..a538416f59 100644 --- a/api/src/backend/tasks/jobs/reports/__init__.py +++ b/api/src/backend/tasks/jobs/reports/__init__.py @@ -17,6 +17,9 @@ from .charts import ( get_chart_color_for_percentage, ) +# Framework-specific generators +from .cis import CISReportGenerator + # Reusable components # Reusable components: Color helpers, Badge components, Risk component, # Table components, Section components @@ -31,10 +34,12 @@ from .components import ( create_section_header, create_status_badge, create_summary_table, + escape_html, get_color_for_compliance, get_color_for_risk_level, get_color_for_weight, get_status_color, + truncate_text, ) # Framework configuration: Main configuration, Color constants, ENS colors, @@ -90,8 +95,6 @@ from .config import ( FrameworkConfig, get_framework_config, ) - -# Framework-specific generators from .csa import CSAReportGenerator from .ens import ENSReportGenerator from .nis2 import NIS2ReportGenerator @@ -109,6 +112,7 @@ __all__ = [ "ENSReportGenerator", "NIS2ReportGenerator", "CSAReportGenerator", + "CISReportGenerator", # Configuration "FrameworkConfig", "FRAMEWORK_REGISTRY", @@ -182,6 +186,9 @@ __all__ = [ # Section components "create_section_header", "create_summary_table", + # Text helpers + "truncate_text", + "escape_html", # Chart functions "get_chart_color_for_percentage", "create_vertical_bar_chart", diff --git a/api/src/backend/tasks/jobs/reports/cis.py b/api/src/backend/tasks/jobs/reports/cis.py new file mode 100644 index 0000000000..0fbb416a17 --- /dev/null +++ b/api/src/backend/tasks/jobs/reports/cis.py @@ -0,0 +1,755 @@ +import os +import re +from collections import defaultdict +from typing import Any + +from reportlab.lib.units import inch +from reportlab.platypus import Image, PageBreak, Paragraph, Spacer, Table, TableStyle + +from api.models import StatusChoices + +from .base import ( + BaseComplianceReportGenerator, + ComplianceData, + RequirementData, + get_requirement_metadata, +) +from .charts import ( + create_horizontal_bar_chart, + create_pie_chart, + create_stacked_bar_chart, + get_chart_color_for_percentage, +) +from .components import ColumnConfig, create_data_table, escape_html, truncate_text +from .config import ( + CHART_COLOR_GREEN_1, + CHART_COLOR_RED, + CHART_COLOR_YELLOW, + COLOR_BG_BLUE, + COLOR_BLUE, + COLOR_BORDER_GRAY, + COLOR_DARK_GRAY, + COLOR_GRAY, + COLOR_GRID_GRAY, + COLOR_HIGH_RISK, + COLOR_LIGHT_BLUE, + COLOR_SAFE, + COLOR_WHITE, +) + +# Ordered buckets used both in the executive summary tables and the charts +# section. Exposed as module constants so the two call sites never drift. +_PROFILE_BUCKET_ORDER: tuple[str, ...] = ("L1", "L2", "Other") +_ASSESSMENT_BUCKET_ORDER: tuple[str, ...] = ("Automated", "Manual") + +# Anchored matchers for profile normalization — substring checks on "L1"/"L2" +# would happily match unrelated tokens like "CL2 Worker" or "HL2" coming from +# future CIS profile enum values. +_LEVEL_2_RE = re.compile(r"(?:\bLevel\s*2\b|\bL2\b|Level_2)") +_LEVEL_1_RE = re.compile(r"(?:\bLevel\s*1\b|\bL1\b|Level_1)") + + +def _normalize_profile(profile: Any) -> str: + """Bucket a CIS Profile enum/string into one of: ``L1``, ``L2``, ``Other``. + + The ``CIS_Requirement_Attribute_Profile`` enum has values like + ``"Level 1"``, ``"Level 2"``, ``"E3 Level 1"``, ``"E5 Level 2"``. We + collapse them into three buckets to keep charts and badges readable + across CIS variants, using anchored regex matches so that future enum + values cannot accidentally promote e.g. ``"CL2 Worker"`` into ``L2``. + + Args: + profile: The profile value (enum member, string, or ``None``). + + Returns: + One of ``"L1"``, ``"L2"``, ``"Other"``. + """ + if profile is None: + return "Other" + value = getattr(profile, "value", None) or str(profile) + if _LEVEL_2_RE.search(value): + return "L2" + if _LEVEL_1_RE.search(value): + return "L1" + return "Other" + + +def _profile_badge_text(bucket: str) -> str: + """Map a normalized profile bucket (L1/L2/Other) to a short badge label.""" + return {"L1": "Level 1", "L2": "Level 2"}.get(bucket, "Other") + + +# ============================================================================= +# CIS Report Generator +# ============================================================================= + + +class CISReportGenerator(BaseComplianceReportGenerator): + """ + PDF report generator for CIS (Center for Internet Security) Benchmarks. + + CIS differs from single-version frameworks (ENS, NIS2, CSA) in that: + - Each provider has multiple CIS versions (e.g. AWS: 1.4, 1.5, ..., 6.0). + - Section names differ across versions and providers and MUST be derived + at runtime from the loaded compliance data. + - Requirements carry Profile (Level 1/Level 2) and AssessmentStatus + (Automated/Manual) attributes that drive the executive summary and + charts. + + This generator produces: + - Cover page with Prowler logo and dynamic CIS version/provider metadata + - Executive summary with overall compliance score, counts, and breakdowns + by Profile and AssessmentStatus + - Charts: overall status pie, pass rate by section (horizontal bar), + Level 1 vs Level 2 pass/fail distribution (stacked bar) + - Requirements index grouped by dynamic section + - Detailed findings for FAIL requirements with CIS-specific audit / + remediation / rationale details + """ + + # Per-run memoization cache for ``_compute_statistics``. ``generate()`` + # is the public entry point and is called once per PDF, so scoping the + # cache to the last seen ComplianceData instance is enough to avoid the + # double computation between executive summary and charts section. + _stats_cache_key: int | None = None + _stats_cache_value: dict | None = None + + # Body section ordering — ensure every top-level section starts on its + # own clean page. The base class only puts a PageBreak AFTER Charts and + # Requirements Index, so Executive Summary and Charts end up sharing a + # page. This override prepends a PageBreak so Compliance Analysis always + # begins on a fresh page. + def _build_body_sections(self, data: ComplianceData) -> list: + return [PageBreak(), *super()._build_body_sections(data)] + + # ------------------------------------------------------------------------- + # Cover page override — shows dynamic CIS version + provider in the title + # ------------------------------------------------------------------------- + + def create_cover_page(self, data: ComplianceData) -> list: + """Create the CIS report cover page with Prowler + CIS logos side by side.""" + elements = [] + + # Create logos side by side (same pattern as NIS2 / ENS) + prowler_logo_path = os.path.join( + os.path.dirname(__file__), "../../assets/img/prowler_logo.png" + ) + cis_logo_path = os.path.join( + os.path.dirname(__file__), "../../assets/img/cis_logo.png" + ) + + if os.path.exists(cis_logo_path): + prowler_logo = Image(prowler_logo_path, width=3.5 * inch, height=0.7 * inch) + cis_logo = Image(cis_logo_path, width=2.3 * inch, height=1.1 * inch) + logos_table = Table( + [[prowler_logo, cis_logo]], colWidths=[4 * inch, 2.5 * inch] + ) + logos_table.setStyle( + TableStyle( + [ + ("ALIGN", (0, 0), (0, 0), "LEFT"), + ("ALIGN", (1, 0), (1, 0), "RIGHT"), + ("VALIGN", (0, 0), (0, 0), "MIDDLE"), + ("VALIGN", (1, 0), (1, 0), "MIDDLE"), + ] + ) + ) + elements.append(logos_table) + elif os.path.exists(prowler_logo_path): + # Fallback: only the Prowler logo if the CIS asset is missing + elements.append(Image(prowler_logo_path, width=5 * inch, height=1 * inch)) + + elements.append(Spacer(1, 0.5 * inch)) + + # Dynamic title: "CIS Benchmark v5.0 — AWS Compliance Report" + provider_label = "" + if data.provider_obj: + provider_label = f" — {data.provider_obj.provider.upper()}" + title_text = ( + f"CIS Benchmark v{data.version}{provider_label}
Compliance Report" + ) + elements.append(Paragraph(title_text, self.styles["title"])) + elements.append(Spacer(1, 0.5 * inch)) + + # Metadata table via base class helper + info_rows = self._build_info_rows(data, language=self.config.language) + metadata_data = [] + for label, value in info_rows: + if label in ("Name:", "Description:") and value: + metadata_data.append( + [label, Paragraph(str(value), self.styles["normal_center"])] + ) + else: + metadata_data.append([label, value]) + + metadata_table = Table(metadata_data, colWidths=[2 * inch, 4 * inch]) + metadata_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (0, -1), COLOR_BLUE), + ("TEXTCOLOR", (0, 0), (0, -1), COLOR_WHITE), + ("FONTNAME", (0, 0), (0, -1), "FiraCode"), + ("BACKGROUND", (1, 0), (1, -1), COLOR_BG_BLUE), + ("TEXTCOLOR", (1, 0), (1, -1), COLOR_GRAY), + ("FONTNAME", (1, 0), (1, -1), "PlusJakartaSans"), + ("ALIGN", (0, 0), (-1, -1), "LEFT"), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("FONTSIZE", (0, 0), (-1, -1), 11), + ("GRID", (0, 0), (-1, -1), 1, COLOR_BORDER_GRAY), + ("LEFTPADDING", (0, 0), (-1, -1), 10), + ("RIGHTPADDING", (0, 0), (-1, -1), 10), + ("TOPPADDING", (0, 0), (-1, -1), 8), + ("BOTTOMPADDING", (0, 0), (-1, -1), 8), + ] + ) + ) + elements.append(metadata_table) + + return elements + + # ------------------------------------------------------------------------- + # Executive Summary + # ------------------------------------------------------------------------- + + def create_executive_summary(self, data: ComplianceData) -> list: + """Create the CIS executive summary section.""" + elements = [] + + elements.append(Paragraph("Executive Summary", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + + stats = self._compute_statistics(data) + + # --- Summary metrics table --- + summary_data = [ + ["Metric", "Value"], + ["Total Requirements", str(stats["total"])], + ["Passed", str(stats["passed"])], + ["Failed", str(stats["failed"])], + ["Manual", str(stats["manual"])], + ["Overall Compliance", f"{stats['overall_compliance']:.1f}%"], + ] + summary_table = Table(summary_data, colWidths=[3 * inch, 2 * inch]) + summary_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE), + ("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE), + ("BACKGROUND", (0, 2), (0, 2), COLOR_SAFE), + ("TEXTCOLOR", (0, 2), (0, 2), COLOR_WHITE), + ("BACKGROUND", (0, 3), (0, 3), COLOR_HIGH_RISK), + ("TEXTCOLOR", (0, 3), (0, 3), COLOR_WHITE), + ("BACKGROUND", (0, 4), (0, 4), COLOR_DARK_GRAY), + ("TEXTCOLOR", (0, 4), (0, 4), COLOR_WHITE), + ("FONTNAME", (0, 0), (-1, 0), "PlusJakartaSans"), + ("FONTSIZE", (0, 0), (-1, 0), 12), + ("FONTSIZE", (0, 1), (-1, -1), 10), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("GRID", (0, 0), (-1, -1), 0.5, COLOR_BORDER_GRAY), + ("BOTTOMPADDING", (0, 0), (-1, 0), 10), + ( + "ROWBACKGROUNDS", + (1, 1), + (1, -1), + [COLOR_WHITE, COLOR_BG_BLUE], + ), + ] + ) + ) + elements.append(summary_table) + elements.append(Spacer(1, 0.25 * inch)) + + # --- Profile breakdown table --- + elements.append(Paragraph("Breakdown by Profile", self.styles["h2"])) + elements.append(Spacer(1, 0.1 * inch)) + profile_counts = stats["profile_counts"] + profile_table_data = [["Profile", "Passed", "Failed", "Manual", "Total"]] + for bucket in _PROFILE_BUCKET_ORDER: + counts = profile_counts.get(bucket, {"passed": 0, "failed": 0, "manual": 0}) + total = counts["passed"] + counts["failed"] + counts["manual"] + if total == 0: + continue + profile_table_data.append( + [ + _profile_badge_text(bucket), + str(counts["passed"]), + str(counts["failed"]), + str(counts["manual"]), + str(total), + ] + ) + profile_table = Table( + profile_table_data, + colWidths=[1.5 * inch, 1 * inch, 1 * inch, 1 * inch, 1 * inch], + ) + profile_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), COLOR_BLUE), + ("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE), + ("FONTNAME", (0, 0), (-1, 0), "FiraCode"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTSIZE", (0, 1), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.5, COLOR_GRID_GRAY), + ( + "ROWBACKGROUNDS", + (0, 1), + (-1, -1), + [COLOR_WHITE, COLOR_BG_BLUE], + ), + ] + ) + ) + elements.append(profile_table) + elements.append(Spacer(1, 0.25 * inch)) + + # --- Assessment status breakdown --- + elements.append(Paragraph("Breakdown by Assessment Status", self.styles["h2"])) + elements.append(Spacer(1, 0.1 * inch)) + assessment_counts = stats["assessment_counts"] + assessment_table_data = [["Assessment", "Passed", "Failed", "Manual", "Total"]] + for bucket in _ASSESSMENT_BUCKET_ORDER: + counts = assessment_counts.get( + bucket, {"passed": 0, "failed": 0, "manual": 0} + ) + total = counts["passed"] + counts["failed"] + counts["manual"] + if total == 0: + continue + assessment_table_data.append( + [ + bucket, + str(counts["passed"]), + str(counts["failed"]), + str(counts["manual"]), + str(total), + ] + ) + assessment_table = Table( + assessment_table_data, + colWidths=[1.5 * inch, 1 * inch, 1 * inch, 1 * inch, 1 * inch], + ) + assessment_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), COLOR_LIGHT_BLUE), + ("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE), + ("FONTNAME", (0, 0), (-1, 0), "FiraCode"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTSIZE", (0, 1), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.5, COLOR_GRID_GRAY), + ( + "ROWBACKGROUNDS", + (0, 1), + (-1, -1), + [COLOR_WHITE, COLOR_BG_BLUE], + ), + ] + ) + ) + elements.append(assessment_table) + elements.append(Spacer(1, 0.25 * inch)) + + # --- Top 5 failing sections --- + top_failing = stats["top_failing_sections"] + if top_failing: + elements.append( + Paragraph("Top Sections with Lowest Compliance", self.styles["h2"]) + ) + elements.append(Spacer(1, 0.1 * inch)) + top_table_data = [["Section", "Passed", "Failed", "Compliance"]] + for section_label, section_stats in top_failing: + passed = section_stats["passed"] + failed = section_stats["failed"] + total = passed + failed + pct = (passed / total * 100) if total > 0 else 100 + top_table_data.append( + [ + truncate_text(section_label, 55), + str(passed), + str(failed), + f"{pct:.1f}%", + ] + ) + top_table = Table( + top_table_data, + colWidths=[3.5 * inch, 0.9 * inch, 0.9 * inch, 1.2 * inch], + ) + top_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), COLOR_HIGH_RISK), + ("TEXTCOLOR", (0, 0), (-1, 0), COLOR_WHITE), + ("FONTNAME", (0, 0), (-1, 0), "FiraCode"), + ("FONTSIZE", (0, 0), (-1, 0), 10), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTSIZE", (0, 1), (-1, -1), 9), + ("GRID", (0, 0), (-1, -1), 0.5, COLOR_GRID_GRAY), + ( + "ROWBACKGROUNDS", + (0, 1), + (-1, -1), + [COLOR_WHITE, COLOR_BG_BLUE], + ), + ] + ) + ) + elements.append(top_table) + + return elements + + # ------------------------------------------------------------------------- + # Charts section + # ------------------------------------------------------------------------- + + def create_charts_section(self, data: ComplianceData) -> list: + """Create the CIS charts section.""" + elements = [] + + elements.append(Paragraph("Compliance Analysis", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + + # --- Pie chart: overall Pass / Fail / Manual --- + stats = self._compute_statistics(data) + pie_labels = [] + pie_values = [] + pie_colors = [] + if stats["passed"] > 0: + pie_labels.append(f"Pass ({stats['passed']})") + pie_values.append(stats["passed"]) + pie_colors.append(CHART_COLOR_GREEN_1) + if stats["failed"] > 0: + pie_labels.append(f"Fail ({stats['failed']})") + pie_values.append(stats["failed"]) + pie_colors.append(CHART_COLOR_RED) + if stats["manual"] > 0: + pie_labels.append(f"Manual ({stats['manual']})") + pie_values.append(stats["manual"]) + pie_colors.append(CHART_COLOR_YELLOW) + + if pie_values: + elements.append(Paragraph("Overall Status Distribution", self.styles["h2"])) + elements.append(Spacer(1, 0.1 * inch)) + pie_buffer = create_pie_chart( + labels=pie_labels, + values=pie_values, + colors=pie_colors, + ) + pie_buffer.seek(0) + elements.append(Image(pie_buffer, width=4.5 * inch, height=4.5 * inch)) + elements.append(Spacer(1, 0.2 * inch)) + + # --- Horizontal bar: pass rate by section --- + section_stats = stats["section_stats"] + if section_stats: + elements.append(PageBreak()) + elements.append(Paragraph("Compliance by Section", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + elements.append( + Paragraph( + "The following chart shows compliance percentage for each CIS " + "section based on automated checks:", + self.styles["normal_center"], + ) + ) + elements.append(Spacer(1, 0.1 * inch)) + + # Sort sections by pass rate descending for readability + sorted_sections = sorted( + section_stats.items(), + key=lambda item: ( + (item[1]["passed"] / (item[1]["passed"] + item[1]["failed"]) * 100) + if (item[1]["passed"] + item[1]["failed"]) > 0 + else 100 + ), + reverse=True, + ) + bar_labels = [] + bar_values = [] + for section_label, section_data in sorted_sections: + total = section_data["passed"] + section_data["failed"] + if total == 0: + continue + pct = (section_data["passed"] / total) * 100 + bar_labels.append(truncate_text(section_label, 60)) + bar_values.append(pct) + + if bar_values: + bar_buffer = create_horizontal_bar_chart( + labels=bar_labels, + values=bar_values, + xlabel="Compliance (%)", + color_func=get_chart_color_for_percentage, + label_fontsize=9, + ) + bar_buffer.seek(0) + elements.append(Image(bar_buffer, width=6.5 * inch, height=5 * inch)) + + # --- Stacked bar: Level 1 vs Level 2 pass/fail --- + profile_counts = stats["profile_counts"] + has_profile_data = any( + (counts["passed"] + counts["failed"]) > 0 + for counts in profile_counts.values() + ) + if has_profile_data: + elements.append(PageBreak()) + elements.append(Paragraph("Profile Breakdown", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + elements.append( + Paragraph( + "Distribution of Pass / Fail / Manual across CIS profile levels.", + self.styles["normal_center"], + ) + ) + elements.append(Spacer(1, 0.1 * inch)) + + profile_labels = [] + pass_series = [] + fail_series = [] + manual_series = [] + for bucket in _PROFILE_BUCKET_ORDER: + counts = profile_counts.get(bucket) + if not counts: + continue + total = counts["passed"] + counts["failed"] + counts["manual"] + if total == 0: + continue + profile_labels.append(_profile_badge_text(bucket)) + pass_series.append(counts["passed"]) + fail_series.append(counts["failed"]) + manual_series.append(counts["manual"]) + + if profile_labels: + stacked_buffer = create_stacked_bar_chart( + labels=profile_labels, + data_series={ + "Pass": pass_series, + "Fail": fail_series, + "Manual": manual_series, + }, + xlabel="Profile", + ylabel="Requirements", + ) + stacked_buffer.seek(0) + elements.append(Image(stacked_buffer, width=6 * inch, height=4 * inch)) + + return elements + + # ------------------------------------------------------------------------- + # Requirements Index + # ------------------------------------------------------------------------- + + def create_requirements_index(self, data: ComplianceData) -> list: + """Create the CIS requirements index grouped by dynamic section.""" + elements = [] + + elements.append(Paragraph("Requirements Index", self.styles["h1"])) + elements.append(Spacer(1, 0.1 * inch)) + + sections = self._derive_sections(data) + by_section: dict[str, list[dict]] = defaultdict(list) + for req in data.requirements: + meta = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + section = "Other" + profile_bucket = "Other" + assessment = "" + if meta: + section = getattr(meta, "Section", "Other") or "Other" + profile_bucket = _normalize_profile(getattr(meta, "Profile", None)) + assessment_enum = getattr(meta, "AssessmentStatus", None) + assessment = getattr(assessment_enum, "value", None) or str( + assessment_enum or "" + ) + by_section[section].append( + { + "id": req.id, + "description": truncate_text(req.description, 80), + "profile": _profile_badge_text(profile_bucket), + "assessment": assessment or "-", + "status": (req.status or "").upper(), + } + ) + + columns = [ + ColumnConfig("ID", 0.9 * inch, "id", align="LEFT"), + ColumnConfig("Description", 3.0 * inch, "description", align="LEFT"), + ColumnConfig("Profile", 0.9 * inch, "profile"), + ColumnConfig("Assessment", 1 * inch, "assessment"), + ColumnConfig("Status", 0.9 * inch, "status"), + ] + + for section in sections: + rows = by_section.get(section, []) + if not rows: + continue + elements.append(Paragraph(truncate_text(section, 90), self.styles["h2"])) + elements.append(Spacer(1, 0.05 * inch)) + table = create_data_table( + data=rows, + columns=columns, + header_color=self.config.primary_color, + normal_style=self.styles["normal_center"], + ) + elements.append(table) + elements.append(Spacer(1, 0.15 * inch)) + + return elements + + # ------------------------------------------------------------------------- + # Detailed findings hook — inject CIS-specific rationale / audit content + # ------------------------------------------------------------------------- + + def _render_requirement_detail_extras( + self, req: RequirementData, data: ComplianceData + ) -> list: + """Render CIS rationale, impact, audit, remediation and references.""" + extras = [] + meta = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if meta is None: + return extras + + field_map = [ + ("Rationale", "RationaleStatement"), + ("Impact", "ImpactStatement"), + ("Audit Procedure", "AuditProcedure"), + ("Remediation", "RemediationProcedure"), + ("References", "References"), + ] + + for label, attr_name in field_map: + value = getattr(meta, attr_name, None) + if not value: + continue + text = str(value).strip() + if not text: + continue + extras.append(Paragraph(f"{label}:", self.styles["h3"])) + extras.append(Paragraph(escape_html(text), self.styles["normal"])) + extras.append(Spacer(1, 0.08 * inch)) + + return extras + + # ------------------------------------------------------------------------- + # Private helpers + # ------------------------------------------------------------------------- + + def _derive_sections(self, data: ComplianceData) -> list[str]: + """Extract ordered unique Section names from loaded compliance data.""" + seen: dict[str, bool] = {} + for req in data.requirements: + meta = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if meta is None: + continue + section = getattr(meta, "Section", None) or "Other" + if section not in seen: + seen[section] = True + return list(seen.keys()) + + def _compute_statistics(self, data: ComplianceData) -> dict: + """Aggregate all statistics needed for summary and charts. + + Memoized per-``ComplianceData`` instance via ``_stats_cache_*``: the + executive summary and the charts section both need the same numbers, + so they would otherwise re-iterate the requirements twice. We key on + ``id(data)`` because ``ComplianceData`` is a dataclass and its + instances are not hashable. + + Returns a dict with: + - total, passed, failed, manual: int + - overall_compliance: float (percentage) + - profile_counts: {"L1": {"passed", "failed", "manual"}, ...} + - assessment_counts: {"Automated": {...}, "Manual": {...}} + - section_stats: {section_name: {"passed", "failed", "manual"}, ...} + - top_failing_sections: list[(section_name, stats)] (up to 5) + """ + cache_key = id(data) + if self._stats_cache_key == cache_key and self._stats_cache_value is not None: + return self._stats_cache_value + stats = self._compute_statistics_uncached(data) + self._stats_cache_key = cache_key + self._stats_cache_value = stats + return stats + + def _compute_statistics_uncached(self, data: ComplianceData) -> dict: + """Actual aggregation kernel; call ``_compute_statistics`` instead.""" + total = len(data.requirements) + passed = sum(1 for r in data.requirements if r.status == StatusChoices.PASS) + failed = sum(1 for r in data.requirements if r.status == StatusChoices.FAIL) + manual = sum(1 for r in data.requirements if r.status == StatusChoices.MANUAL) + + evaluated = passed + failed + overall_compliance = (passed / evaluated * 100) if evaluated > 0 else 100.0 + + profile_counts: dict[str, dict[str, int]] = { + "L1": {"passed": 0, "failed": 0, "manual": 0}, + "L2": {"passed": 0, "failed": 0, "manual": 0}, + "Other": {"passed": 0, "failed": 0, "manual": 0}, + } + assessment_counts: dict[str, dict[str, int]] = { + "Automated": {"passed": 0, "failed": 0, "manual": 0}, + "Manual": {"passed": 0, "failed": 0, "manual": 0}, + } + section_stats: dict[str, dict[str, int]] = defaultdict( + lambda: {"passed": 0, "failed": 0, "manual": 0} + ) + + for req in data.requirements: + meta = get_requirement_metadata(req.id, data.attributes_by_requirement_id) + if meta is None: + continue + + profile_bucket = _normalize_profile(getattr(meta, "Profile", None)) + assessment_enum = getattr(meta, "AssessmentStatus", None) + assessment_value = getattr(assessment_enum, "value", None) or str( + assessment_enum or "" + ) + assessment_bucket = ( + "Automated" if assessment_value == "Automated" else "Manual" + ) + section = getattr(meta, "Section", None) or "Other" + + status_key = { + StatusChoices.PASS: "passed", + StatusChoices.FAIL: "failed", + StatusChoices.MANUAL: "manual", + }.get(req.status) + if status_key is None: + continue + + profile_counts[profile_bucket][status_key] += 1 + assessment_counts[assessment_bucket][status_key] += 1 + section_stats[section][status_key] += 1 + + # Top 5 sections with lowest pass rate (only sections with evaluated reqs) + def _section_rate(item): + _, stats_ = item + evaluated_ = stats_["passed"] + stats_["failed"] + if evaluated_ == 0: + return 101 # sort evaluated=0 to the bottom + return stats_["passed"] / evaluated_ * 100 + + top_failing_sections = sorted( + ( + item + for item in section_stats.items() + if (item[1]["passed"] + item[1]["failed"]) > 0 + ), + key=_section_rate, + )[:5] + + return { + "total": total, + "passed": passed, + "failed": failed, + "manual": manual, + "overall_compliance": overall_compliance, + "profile_counts": profile_counts, + "assessment_counts": assessment_counts, + "section_stats": dict(section_stats), + "top_failing_sections": top_failing_sections, + } diff --git a/api/src/backend/tasks/jobs/reports/components.py b/api/src/backend/tasks/jobs/reports/components.py index 323c4547e6..049cc043d3 100644 --- a/api/src/backend/tasks/jobs/reports/components.py +++ b/api/src/backend/tasks/jobs/reports/components.py @@ -26,6 +26,52 @@ from .config import ( ) +def truncate_text(text: str, max_len: int) -> str: + """Truncate ``text`` to ``max_len`` characters, appending an ellipsis if cut. + + Used by report generators that need to squeeze long descriptions, section + titles or finding titles into a fixed-width table cell. + + Args: + text: Source string. ``None`` and non-string values are treated as empty. + max_len: Maximum output length including the ellipsis. Values < 4 are + clamped so the result never grows beyond ``max_len``. + + Returns: + The original string if short enough, otherwise ``text[: max_len - 3] + "..."``. + When ``max_len < 4`` a plain substring of length ``max_len`` is returned + so callers never get a string longer than they asked for. + """ + if not text: + return "" + text = str(text) + if len(text) <= max_len: + return text + if max_len < 4: + return text[:max_len] + return text[: max_len - 3] + "..." + + +def escape_html(text: str) -> str: + """Escape the minimal HTML entities required for safe ReportLab Paragraph rendering. + + ReportLab's ``Paragraph`` parses a small HTML subset, so raw ``<``, ``>`` + and ``&`` in user-provided content (rationale, remediation, etc.) would + break layout or be interpreted as tags. This helper mirrors + ``html.escape`` but avoids pulling in the stdlib dependency and keeps the + output deterministic. + + Args: + text: Untrusted source string. + + Returns: + A string safe to embed inside a ReportLab Paragraph. + """ + return ( + str(text or "").replace("&", "&").replace("<", "<").replace(">", ">") + ) + + def get_color_for_risk_level(risk_level: int) -> colors.Color: """ Get color based on risk level. diff --git a/api/src/backend/tasks/jobs/reports/config.py b/api/src/backend/tasks/jobs/reports/config.py index fe0326980d..669f31c287 100644 --- a/api/src/backend/tasks/jobs/reports/config.py +++ b/api/src/backend/tasks/jobs/reports/config.py @@ -313,6 +313,32 @@ FRAMEWORK_REGISTRY: dict[str, FrameworkConfig] = { has_niveles=False, has_weight=False, ), + "cis": FrameworkConfig( + name="cis", + display_name="CIS Benchmark", + logo_filename=None, + primary_color=COLOR_BLUE, + secondary_color=COLOR_LIGHT_BLUE, + bg_color=COLOR_BG_BLUE, + attribute_fields=[ + "Section", + "SubSection", + "Profile", + "AssessmentStatus", + "Description", + "RationaleStatement", + "ImpactStatement", + "RemediationProcedure", + "AuditProcedure", + "References", + ], + sections=None, # Derived dynamically per CIS variant (section names differ across versions/providers) + language="en", + has_risk_levels=False, + has_dimensions=False, + has_niveles=False, + has_weight=False, + ), } @@ -336,5 +362,7 @@ def get_framework_config(compliance_id: str) -> FrameworkConfig | None: return FRAMEWORK_REGISTRY["nis2"] if "csa" in compliance_lower or "ccm" in compliance_lower: return FRAMEWORK_REGISTRY["csa_ccm"] + if compliance_lower.startswith("cis_") or "cis" in compliance_lower: + return FRAMEWORK_REGISTRY["cis"] return None diff --git a/api/src/backend/tasks/tasks.py b/api/src/backend/tasks/tasks.py index b4a8f91668..e22eb2d14d 100644 --- a/api/src/backend/tasks/tasks.py +++ b/api/src/backend/tasks/tasks.py @@ -1000,13 +1000,17 @@ def jira_integration_task( @handle_provider_deletion def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id: str): """ - Optimized task to generate ThreatScore, ENS, NIS2, and CSA CCM reports with shared queries. + Optimized task to generate ThreatScore, ENS, NIS2, CSA CCM and CIS reports with shared queries. This task is more efficient than running separate report tasks because it reuses database queries: - Provider object fetched once (instead of multiple times) - Requirement statistics aggregated once (instead of multiple times) - Can reduce database load by up to 50-70% + CIS emits a single PDF per run: the one matching the highest CIS version + available for the scan's provider, picked dynamically from + ``Compliance.get_bulk`` (no hard-coded provider → version mapping). + Args: tenant_id (str): The tenant identifier. scan_id (str): The scan identifier. @@ -1023,6 +1027,7 @@ def generate_compliance_reports_task(tenant_id: str, scan_id: str, provider_id: generate_ens=True, generate_nis2=True, generate_csa=True, + generate_cis=True, ) diff --git a/api/src/backend/tasks/tests/test_reports.py b/api/src/backend/tasks/tests/test_reports.py index 858f4c06ca..a25df876f3 100644 --- a/api/src/backend/tasks/tests/test_reports.py +++ b/api/src/backend/tasks/tests/test_reports.py @@ -4,7 +4,11 @@ from unittest.mock import Mock, patch import matplotlib import pytest from reportlab.lib import colors -from tasks.jobs.report import generate_compliance_reports, generate_threatscore_report +from tasks.jobs.report import ( + _pick_latest_cis_variant, + generate_compliance_reports, + generate_threatscore_report, +) from tasks.jobs.reports import ( CHART_COLOR_GREEN_1, CHART_COLOR_GREEN_2, @@ -422,6 +426,266 @@ class TestGenerateComplianceReportsOptimized: mock_ens.assert_not_called() mock_nis2.assert_not_called() + @patch("tasks.jobs.report._upload_to_s3") + @patch("tasks.jobs.report.generate_cis_report") + def test_no_findings_returns_flat_cis_entry( + self, + mock_cis, + mock_upload, + tenants_fixture, + scans_fixture, + providers_fixture, + ): + """Scan with no findings and ``generate_cis=True`` must yield a flat + ``{"upload": False, "path": ""}`` entry, consistent with the other + frameworks (no nested dict, no sentinel keys).""" + tenant = tenants_fixture[0] + scan = scans_fixture[0] + provider = providers_fixture[0] + + result = generate_compliance_reports( + tenant_id=str(tenant.id), + scan_id=str(scan.id), + provider_id=str(provider.id), + generate_threatscore=False, + generate_ens=False, + generate_nis2=False, + generate_csa=False, + generate_cis=True, + ) + + assert result["cis"] == {"upload": False, "path": ""} + mock_cis.assert_not_called() + + +@pytest.mark.django_db +class TestGenerateComplianceReportsCIS: + """Test suite covering the CIS branch of generate_compliance_reports.""" + + def _force_scan_has_findings(self, monkeypatch): + """Bypass the ScanSummary.exists() early-return guard.""" + + class _FakeManager: + def filter(self, **kwargs): + class _Q: + def exists(self): + return True + + return _Q() + + monkeypatch.setattr("tasks.jobs.report.ScanSummary.objects", _FakeManager()) + + @patch("tasks.jobs.report._aggregate_requirement_statistics_from_database") + @patch("tasks.jobs.report._upload_to_s3") + @patch("tasks.jobs.report.generate_cis_report") + @patch("tasks.jobs.report.Compliance.get_bulk") + def test_cis_picks_latest_version( + self, + mock_get_bulk, + mock_cis, + mock_upload, + mock_stats, + monkeypatch, + tenants_fixture, + scans_fixture, + providers_fixture, + ): + """CIS branch should generate a single PDF for the highest version. + + The returned ``results["cis"]`` must have the same flat shape as the + other single-version frameworks (``{"upload", "path"}``) — the picked + variant is an internal detail and is not exposed in the result. + """ + tenant = tenants_fixture[0] + scan = scans_fixture[0] + provider = providers_fixture[0] + + self._force_scan_has_findings(monkeypatch) + + mock_stats.return_value = {} + # Multiple CIS variants + a non-CIS framework that must be ignored. + # Includes 1.10 to verify the selection is not lexicographic. + mock_get_bulk.return_value = { + "cis_1.4_aws": Mock(), + "cis_1.10_aws": Mock(), + "cis_2.0_aws": Mock(), + "cis_5.0_aws": Mock(), + "ens_rd2022_aws": Mock(), + } + mock_upload.return_value = "s3://bucket/path" + + result = generate_compliance_reports( + tenant_id=str(tenant.id), + scan_id=str(scan.id), + provider_id=str(provider.id), + generate_threatscore=False, + generate_ens=False, + generate_nis2=False, + generate_csa=False, + generate_cis=True, + ) + + # Exactly one call for the latest version, never for older variants + # or non-CIS frameworks. + assert mock_cis.call_count == 1 + assert mock_cis.call_args.kwargs["compliance_id"] == "cis_5.0_aws" + + assert result["cis"]["upload"] is True + assert result["cis"]["path"] == "s3://bucket/path" + assert "compliance_id" not in result["cis"] + + @patch("tasks.jobs.report._aggregate_requirement_statistics_from_database") + @patch("tasks.jobs.report._upload_to_s3") + @patch("tasks.jobs.report.generate_cis_report") + @patch("tasks.jobs.report.Compliance.get_bulk") + def test_cis_latest_variant_failure_captured_in_results( + self, + mock_get_bulk, + mock_cis, + mock_upload, + mock_stats, + monkeypatch, + tenants_fixture, + scans_fixture, + providers_fixture, + ): + """A failure in the latest CIS variant must be surfaced in the flat results entry.""" + tenant = tenants_fixture[0] + scan = scans_fixture[0] + provider = providers_fixture[0] + + self._force_scan_has_findings(monkeypatch) + + mock_stats.return_value = {} + mock_get_bulk.return_value = { + "cis_1.4_aws": Mock(), + "cis_5.0_aws": Mock(), + } + mock_cis.side_effect = RuntimeError("boom") + + result = generate_compliance_reports( + tenant_id=str(tenant.id), + scan_id=str(scan.id), + provider_id=str(provider.id), + generate_threatscore=False, + generate_ens=False, + generate_nis2=False, + generate_csa=False, + generate_cis=True, + ) + + # Only the latest variant is attempted; its failure lands in a flat + # entry keyed under "cis" with the same shape as sibling frameworks. + assert mock_cis.call_count == 1 + assert result["cis"]["upload"] is False + assert result["cis"]["error"] == "boom" + assert "compliance_id" not in result["cis"] + + @patch("tasks.jobs.report._aggregate_requirement_statistics_from_database") + @patch("tasks.jobs.report._upload_to_s3") + @patch("tasks.jobs.report.generate_cis_report") + @patch("tasks.jobs.report.Compliance.get_bulk") + def test_cis_provider_without_cis_skipped_cleanly( + self, + mock_get_bulk, + mock_cis, + mock_upload, + mock_stats, + monkeypatch, + tenants_fixture, + scans_fixture, + providers_fixture, + ): + """When ``Compliance.get_bulk`` returns no CIS entry the CIS branch + must skip cleanly and record a flat ``{"upload": False, "path": ""}`` + entry — no hard-coded provider whitelist is consulted.""" + tenant = tenants_fixture[0] + scan = scans_fixture[0] + provider = providers_fixture[0] + + self._force_scan_has_findings(monkeypatch) + mock_stats.return_value = {} + # No ``cis_*`` keys in the bulk → no variant picked. + mock_get_bulk.return_value = {"ens_rd2022_aws": Mock()} + + result = generate_compliance_reports( + tenant_id=str(tenant.id), + scan_id=str(scan.id), + provider_id=str(provider.id), + generate_threatscore=False, + generate_ens=False, + generate_nis2=False, + generate_csa=False, + generate_cis=True, + ) + + assert result["cis"] == {"upload": False, "path": ""} + mock_cis.assert_not_called() + + +class TestPickLatestCisVariant: + """Unit tests for `_pick_latest_cis_variant` helper.""" + + def test_empty_returns_none(self): + assert _pick_latest_cis_variant([]) is None + + def test_single_variant(self): + assert _pick_latest_cis_variant(["cis_5.0_aws"]) == "cis_5.0_aws" + + def test_numeric_not_lexicographic(self): + """1.10 must beat 1.2 (lex sort would pick 1.2).""" + variants = ["cis_1.2_kubernetes", "cis_1.10_kubernetes"] + assert _pick_latest_cis_variant(variants) == "cis_1.10_kubernetes" + + def test_major_version_wins(self): + variants = ["cis_1.4_aws", "cis_2.0_aws", "cis_5.0_aws", "cis_6.0_aws"] + assert _pick_latest_cis_variant(variants) == "cis_6.0_aws" + + def test_minor_version_breaks_tie(self): + variants = ["cis_3.0_aws", "cis_3.1_aws", "cis_2.9_aws"] + assert _pick_latest_cis_variant(variants) == "cis_3.1_aws" + + def test_three_part_version(self): + """Versions like 3.0.1 must win over 3.0.""" + variants = ["cis_3.0_aws", "cis_3.0.1_aws"] + assert _pick_latest_cis_variant(variants) == "cis_3.0.1_aws" + + def test_malformed_names_ignored(self): + variants = ["notcis_1.0_aws", "cis_abc_aws", "cis_5.0_aws"] + assert _pick_latest_cis_variant(variants) == "cis_5.0_aws" + + def test_only_malformed_returns_none(self): + variants = ["notcis_1.0_aws", "cis_abc_aws"] + assert _pick_latest_cis_variant(variants) is None + + def test_multidigit_provider_name(self): + """Provider name with underscores (e.g. googleworkspace) must parse.""" + variants = ["cis_1.3_googleworkspace"] + assert _pick_latest_cis_variant(variants) == "cis_1.3_googleworkspace" + + def test_accepts_iterator(self): + """The helper must accept any iterable, not just lists.""" + + def _gen(): + yield "cis_1.4_aws" + yield "cis_5.0_aws" + + assert _pick_latest_cis_variant(_gen()) == "cis_5.0_aws" + + def test_rejects_single_integer_version(self): + """The regex requires at least one dotted component. ``cis_5_aws`` + without a minor version is malformed per the backend contract.""" + assert _pick_latest_cis_variant(["cis_5_aws"]) is None + + def test_rejects_trailing_dot(self): + """Inputs like ``cis_5._aws`` must be rejected at the regex stage + instead of silently normalising to ``(5, 0)``.""" + assert _pick_latest_cis_variant(["cis_5._aws", "cis_1.0_aws"]) == "cis_1.0_aws" + + def test_rejects_lone_dot_version(self): + """``cis_._aws`` has no numeric component and must be skipped.""" + assert _pick_latest_cis_variant(["cis_._aws", "cis_1.0_aws"]) == "cis_1.0_aws" + class TestOptimizationImprovements: """Test suite for optimization-related functionality.""" diff --git a/api/src/backend/tasks/tests/test_reports_cis.py b/api/src/backend/tasks/tests/test_reports_cis.py new file mode 100644 index 0000000000..2d4528c82d --- /dev/null +++ b/api/src/backend/tasks/tests/test_reports_cis.py @@ -0,0 +1,532 @@ +from unittest.mock import Mock, patch + +import pytest +from reportlab.platypus import Image, LongTable, Paragraph, Table +from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData +from tasks.jobs.reports.cis import ( + CISReportGenerator, + _normalize_profile, + _profile_badge_text, +) + +from api.models import StatusChoices + +# ============================================================================= +# Fixtures +# ============================================================================= + + +@pytest.fixture +def cis_generator(): + """Create a CISReportGenerator instance for testing.""" + config = FRAMEWORK_REGISTRY["cis"] + return CISReportGenerator(config) + + +def _make_attr( + section: str, + profile_value: str = "Level 1", + assessment_value: str = "Automated", + sub_section: str = "", + **extras, +) -> Mock: + """Build a mock CIS_Requirement_Attribute with duck-typed fields.""" + attr = Mock() + attr.Section = section + attr.SubSection = sub_section + # CIS enums have `.value`. Use a simple Mock that exposes `.value`. + attr.Profile = Mock(value=profile_value) + attr.AssessmentStatus = Mock(value=assessment_value) + attr.Description = extras.get("description", "desc") + attr.RationaleStatement = extras.get("rationale", "the rationale") + attr.ImpactStatement = extras.get("impact", "the impact") + attr.RemediationProcedure = extras.get("remediation", "the remediation") + attr.AuditProcedure = extras.get("audit", "the audit") + attr.AdditionalInformation = "" + attr.DefaultValue = "" + attr.References = extras.get("references", "https://example.com") + return attr + + +@pytest.fixture +def basic_cis_compliance_data(): + """Create basic ComplianceData for CIS testing (no requirements).""" + return ComplianceData( + tenant_id="tenant-123", + scan_id="scan-456", + provider_id="provider-789", + compliance_id="cis_5.0_aws", + framework="CIS", + name="CIS Amazon Web Services Foundations Benchmark v5.0.0", + version="5.0", + description="Center for Internet Security AWS Foundations Benchmark", + ) + + +@pytest.fixture +def populated_cis_compliance_data(basic_cis_compliance_data): + """CIS data with mixed requirements across 2 sections, Profile L1/L2, Pass/Fail/Manual.""" + data = basic_cis_compliance_data + data.requirements = [ + RequirementData( + id="1.1", + description="Maintain current contact details", + status=StatusChoices.PASS, + passed_findings=5, + failed_findings=0, + total_findings=5, + checks=["aws_check_1"], + ), + RequirementData( + id="1.2", + description="Ensure root account has no access keys", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=3, + total_findings=3, + checks=["aws_check_2"], + ), + RequirementData( + id="1.3", + description="Ensure MFA is enabled for all IAM users", + status=StatusChoices.MANUAL, + checks=[], + ), + RequirementData( + id="2.1", + description="Ensure S3 Buckets are logging", + status=StatusChoices.PASS, + passed_findings=2, + failed_findings=0, + total_findings=2, + checks=["aws_check_3"], + ), + RequirementData( + id="2.2", + description="Ensure encryption at rest is enabled", + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=4, + total_findings=4, + checks=["aws_check_4"], + ), + ] + data.attributes_by_requirement_id = { + "1.1": { + "attributes": { + "req_attributes": [ + _make_attr( + "1 Identity and Access Management", + profile_value="Level 1", + assessment_value="Automated", + ) + ], + "checks": ["aws_check_1"], + } + }, + "1.2": { + "attributes": { + "req_attributes": [ + _make_attr( + "1 Identity and Access Management", + profile_value="Level 1", + assessment_value="Automated", + ) + ], + "checks": ["aws_check_2"], + } + }, + "1.3": { + "attributes": { + "req_attributes": [ + _make_attr( + "1 Identity and Access Management", + profile_value="Level 2", + assessment_value="Manual", + ) + ], + "checks": [], + } + }, + "2.1": { + "attributes": { + "req_attributes": [ + _make_attr( + "2 Storage", + profile_value="Level 2", + assessment_value="Automated", + ) + ], + "checks": ["aws_check_3"], + } + }, + "2.2": { + "attributes": { + "req_attributes": [ + _make_attr( + "2 Storage", + profile_value="Level 1", + assessment_value="Automated", + ) + ], + "checks": ["aws_check_4"], + } + }, + } + return data + + +# ============================================================================= +# Helper function tests +# ============================================================================= + + +class TestNormalizeProfile: + """Test suite for _normalize_profile helper.""" + + def test_level_1_string(self): + assert _normalize_profile(Mock(value="Level 1")) == "L1" + + def test_level_2_string(self): + assert _normalize_profile(Mock(value="Level 2")) == "L2" + + def test_e3_level_1(self): + assert _normalize_profile(Mock(value="E3 Level 1")) == "L1" + + def test_e5_level_2(self): + assert _normalize_profile(Mock(value="E5 Level 2")) == "L2" + + def test_none_returns_other(self): + assert _normalize_profile(None) == "Other" + + def test_substring_trap_rejected(self): + """Unrelated tokens containing the literal ``L2`` must NOT map to L2.""" + # A future enum value like "CL2 Kubernetes Worker" would be silently + # misclassified by a naive substring check. + assert _normalize_profile(Mock(value="CL2 Worker")) == "Other" + assert _normalize_profile(Mock(value="HL2 Legacy")) == "Other" + + def test_raw_string_level_1(self): + # Mock without .value falls back to str(profile); use a real string + class NoValue: + def __str__(self): + return "Level 1" + + assert _normalize_profile(NoValue()) == "L1" + + def test_unknown_profile_returns_other(self): + assert _normalize_profile(Mock(value="Custom Profile")) == "Other" + + +class TestProfileBadgeText: + def test_l1_label(self): + assert _profile_badge_text("L1") == "Level 1" + + def test_l2_label(self): + assert _profile_badge_text("L2") == "Level 2" + + def test_other_label(self): + assert _profile_badge_text("Other") == "Other" + + +# ============================================================================= +# Generator initialization +# ============================================================================= + + +class TestCISGeneratorInitialization: + def test_generator_created(self, cis_generator): + assert cis_generator is not None + assert cis_generator.config.name == "cis" + + def test_generator_language(self, cis_generator): + assert cis_generator.config.language == "en" + + def test_generator_sections_dynamic(self, cis_generator): + # CIS sections differ per variant so config.sections MUST be None + assert cis_generator.config.sections is None + + def test_attribute_fields_contain_cis_specific(self, cis_generator): + for field in ("Profile", "AssessmentStatus", "RationaleStatement"): + assert field in cis_generator.config.attribute_fields + + +# ============================================================================= +# _derive_sections +# ============================================================================= + + +class TestDeriveSections: + def test_preserves_first_seen_order( + self, cis_generator, populated_cis_compliance_data + ): + sections = cis_generator._derive_sections(populated_cis_compliance_data) + assert sections == [ + "1 Identity and Access Management", + "2 Storage", + ] + + def test_deduplicates_sections(self, cis_generator, basic_cis_compliance_data): + basic_cis_compliance_data.requirements = [ + RequirementData(id="1.1", description="a", status=StatusChoices.PASS), + RequirementData(id="1.2", description="b", status=StatusChoices.PASS), + ] + attr = _make_attr("1 IAM") + basic_cis_compliance_data.attributes_by_requirement_id = { + "1.1": {"attributes": {"req_attributes": [attr], "checks": []}}, + "1.2": {"attributes": {"req_attributes": [attr], "checks": []}}, + } + assert cis_generator._derive_sections(basic_cis_compliance_data) == ["1 IAM"] + + def test_empty_data_returns_empty(self, cis_generator, basic_cis_compliance_data): + basic_cis_compliance_data.requirements = [] + basic_cis_compliance_data.attributes_by_requirement_id = {} + assert cis_generator._derive_sections(basic_cis_compliance_data) == [] + + +# ============================================================================= +# _compute_statistics +# ============================================================================= + + +class TestComputeStatistics: + def test_totals(self, cis_generator, populated_cis_compliance_data): + stats = cis_generator._compute_statistics(populated_cis_compliance_data) + assert stats["total"] == 5 + assert stats["passed"] == 2 + assert stats["failed"] == 2 + assert stats["manual"] == 1 + + def test_overall_compliance_excludes_manual( + self, cis_generator, populated_cis_compliance_data + ): + stats = cis_generator._compute_statistics(populated_cis_compliance_data) + # 2 passed / 4 evaluated (pass + fail) = 50% + assert stats["overall_compliance"] == pytest.approx(50.0) + + def test_overall_compliance_all_manual( + self, cis_generator, basic_cis_compliance_data + ): + basic_cis_compliance_data.requirements = [ + RequirementData(id="x", description="d", status=StatusChoices.MANUAL), + ] + attr = _make_attr("1 IAM", profile_value="Level 1", assessment_value="Manual") + basic_cis_compliance_data.attributes_by_requirement_id = { + "x": {"attributes": {"req_attributes": [attr], "checks": []}}, + } + stats = cis_generator._compute_statistics(basic_cis_compliance_data) + # No evaluated → defaults to 100% + assert stats["overall_compliance"] == 100.0 + + def test_profile_counts(self, cis_generator, populated_cis_compliance_data): + stats = cis_generator._compute_statistics(populated_cis_compliance_data) + profile = stats["profile_counts"] + # From fixture: + # L1: 1.1 (PASS, Auto), 1.2 (FAIL, Auto), 2.2 (FAIL, Auto) → pass=1, fail=2, manual=0 + # L2: 1.3 (MANUAL, Manual), 2.1 (PASS, Auto) → pass=1, fail=0, manual=1 + assert profile["L1"] == {"passed": 1, "failed": 2, "manual": 0} + assert profile["L2"] == {"passed": 1, "failed": 0, "manual": 1} + + def test_assessment_counts(self, cis_generator, populated_cis_compliance_data): + stats = cis_generator._compute_statistics(populated_cis_compliance_data) + assessment = stats["assessment_counts"] + # Automated: 1.1 PASS, 1.2 FAIL, 2.1 PASS, 2.2 FAIL → pass=2, fail=2, manual=0 + # Manual: 1.3 MANUAL → pass=0, fail=0, manual=1 + assert assessment["Automated"] == {"passed": 2, "failed": 2, "manual": 0} + assert assessment["Manual"] == {"passed": 0, "failed": 0, "manual": 1} + + def test_top_failing_sections_includes_all_evaluated( + self, cis_generator, populated_cis_compliance_data + ): + stats = cis_generator._compute_statistics(populated_cis_compliance_data) + top = stats["top_failing_sections"] + # Both sections have 1 PASS + 1 FAIL evaluated → tied at 50%. The + # sort is stable, so both must appear and both must be capped at + # 5 entries. + assert len(top) == 2 + section_names = {name for name, _ in top} + assert section_names == { + "1 Identity and Access Management", + "2 Storage", + } + + def test_compute_statistics_is_memoized( + self, cis_generator, populated_cis_compliance_data + ): + """Calling ``_compute_statistics`` twice with the same data must + reuse the cached value and not re-run the uncached kernel.""" + with patch.object( + CISReportGenerator, + "_compute_statistics_uncached", + wraps=cis_generator._compute_statistics_uncached, + ) as spy: + cis_generator._compute_statistics(populated_cis_compliance_data) + cis_generator._compute_statistics(populated_cis_compliance_data) + assert spy.call_count == 1 + + +# ============================================================================= +# Executive summary +# ============================================================================= + + +class TestCISExecutiveSummary: + def test_title_present(self, cis_generator, populated_cis_compliance_data): + elements = cis_generator.create_executive_summary(populated_cis_compliance_data) + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + text = " ".join(str(p.text) for p in paragraphs) + assert "Executive Summary" in text + + def test_tables_rendered(self, cis_generator, populated_cis_compliance_data): + elements = cis_generator.create_executive_summary(populated_cis_compliance_data) + tables = [e for e in elements if isinstance(e, Table)] + # Exact count: Summary, Profile, Assessment, Top Failing Sections = 4. + assert len(tables) == 4 + + def test_no_requirements(self, cis_generator, basic_cis_compliance_data): + basic_cis_compliance_data.requirements = [] + basic_cis_compliance_data.attributes_by_requirement_id = {} + elements = cis_generator.create_executive_summary(basic_cis_compliance_data) + # With no requirements: Summary table always renders, and both Profile + # and Assessment breakdown tables render with a 0-filled default row, + # but Top Failing Sections is suppressed → exactly 3 tables. + tables = [e for e in elements if isinstance(e, Table)] + assert len(tables) == 3 + + +# ============================================================================= +# Charts section +# ============================================================================= + + +class TestCISChartsSection: + def test_charts_rendered(self, cis_generator, populated_cis_compliance_data): + elements = cis_generator.create_charts_section(populated_cis_compliance_data) + # At least 1 image for the pie + 1 for section bar + 1 for stacked + images = [e for e in elements if isinstance(e, Image)] + assert len(images) >= 1 + + def test_charts_no_data_no_crash(self, cis_generator, basic_cis_compliance_data): + basic_cis_compliance_data.requirements = [] + basic_cis_compliance_data.attributes_by_requirement_id = {} + elements = cis_generator.create_charts_section(basic_cis_compliance_data) + # Must not raise; may or may not have any Image + assert isinstance(elements, list) + + +# ============================================================================= +# Requirements index +# ============================================================================= + + +class TestCISRequirementsIndex: + def test_title_present(self, cis_generator, populated_cis_compliance_data): + elements = cis_generator.create_requirements_index( + populated_cis_compliance_data + ) + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + text = " ".join(str(p.text) for p in paragraphs) + assert "Requirements Index" in text + + def test_groups_by_section(self, cis_generator, populated_cis_compliance_data): + elements = cis_generator.create_requirements_index( + populated_cis_compliance_data + ) + paragraphs = [e for e in elements if isinstance(e, Paragraph)] + text = " ".join(str(p.text) for p in paragraphs) + assert "1 Identity and Access Management" in text + assert "2 Storage" in text + + def test_renders_tables_per_section( + self, cis_generator, populated_cis_compliance_data + ): + elements = cis_generator.create_requirements_index( + populated_cis_compliance_data + ) + # One table per section with requirements. ``create_data_table`` + # returns a LongTable when the row count exceeds its threshold and a + # plain Table otherwise — both are valid. + tables = [e for e in elements if isinstance(e, (Table, LongTable))] + assert len(tables) == 2 + + +# ============================================================================= +# Detailed findings extras hook +# ============================================================================= + + +class TestRenderRequirementDetailExtras: + def test_inserts_all_fields(self, cis_generator, populated_cis_compliance_data): + req = populated_cis_compliance_data.requirements[1] # 1.2 FAIL + extras = cis_generator._render_requirement_detail_extras( + req, populated_cis_compliance_data + ) + text = " ".join(str(p.text) for p in extras if isinstance(p, Paragraph)) + assert "Rationale" in text + assert "Impact" in text + assert "Audit Procedure" in text + assert "Remediation" in text + assert "References" in text + + def test_missing_metadata_returns_empty( + self, cis_generator, basic_cis_compliance_data + ): + basic_cis_compliance_data.attributes_by_requirement_id = {} + req = RequirementData(id="99", description="unknown", status=StatusChoices.FAIL) + extras = cis_generator._render_requirement_detail_extras( + req, basic_cis_compliance_data + ) + assert extras == [] + + def test_escapes_html_chars(self, cis_generator, basic_cis_compliance_data): + attr = _make_attr( + "1 IAM", + rationale="", + ) + basic_cis_compliance_data.attributes_by_requirement_id = { + "1.1": {"attributes": {"req_attributes": [attr], "checks": []}} + } + req = RequirementData(id="1.1", description="d", status=StatusChoices.FAIL) + extras = cis_generator._render_requirement_detail_extras( + req, basic_cis_compliance_data + ) + text = " ".join(str(p.text) for p in extras if isinstance(p, Paragraph)) + assert "