diff --git a/README.md b/README.md index 6513f08..65e0163 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,9 @@ project page is left out. The report has these parts: 1. the cover and a table of contents, 2. a summary and the most critical issues across all sources, -3. the dependency vulnerabilities from the VDR, -4. one section per SARIF run, named after its tool (e.g. CipherCell Lynkeus, CipherCell SAST), +3. the dependency vulnerabilities from the VDR, as CipherCell SCA, +4. one section per SARIF run, named after its tool: CipherCell Lynkeus, then CipherCell SAST, then + any other tool in the order its run appears, 5. at the end, *False Positives and Suppressed Findings*: every result Polygraph classified as a false positive or that carries a SARIF suppression (such as the areas Lynkeus ruled out), with the reason. diff --git a/report_generator/context.py b/report_generator/context.py index e683f38..8ff8768 100644 --- a/report_generator/context.py +++ b/report_generator/context.py @@ -44,6 +44,13 @@ BARS = 10 SEVERITY_ORDER = ("Critical", "High", "Medium", "Low") +# What the dependency check is called wherever the report names its source. +SCA = "CipherCell SCA" + +# The order the sources are reported in, everywhere: the dependency check first, then the SARIF +# tools in this order. A tool not named here follows them, in the order its run appears. +TOOL_ORDER = ("CipherCell Lynkeus", "CipherCell SAST") + # A full git commit SHA. _COMMIT = re.compile(r"[0-9a-f]{40}") @@ -77,7 +84,8 @@ def build_context( context: dict[str, Any] = { "cover": _cover(sources, metadata, tools, sca, project=project, manufacturer=manufacturer), "summary": _summary(tools, sca, documents.get("metrics")), - "sections": [_tool_section(name, report) for name, report in tools], + "sections": ([_sca_section(sca)] if sca is not None else []) + + [_tool_section(name, report) for name, report in tools], } # Without the pipeline's metadata or cloc's count this page would be nothing but gaps. @@ -89,9 +97,6 @@ def build_context( if critical["table"]["rows"]: context["critical"] = critical - if sca is not None: - context["sections"].insert(0, _sca_section(sca)) - set_aside = _set_aside_section(tools) if set_aside is not None: @@ -109,7 +114,9 @@ def _tools(sarif: Any) -> list[tuple[str, SarifReport]]: # noqa: ANN401 runs = [run for run in sarif.get("runs") or [] if isinstance(run, dict)] - return [(_tool_name(run), _summarize(run)) for run in runs] + tools = [(_tool_name(run), _summarize(run)) for run in runs] + + return sorted(tools, key=lambda pair: TOOL_ORDER.index(pair[0]) if pair[0] in TOOL_ORDER else len(TOOL_ORDER)) def _summarize(run: dict[str, Any]) -> SarifReport: @@ -182,7 +189,7 @@ def _cover( manufacturer: str | None, ) -> dict[str, Any]: """The first page.""" - technologies = [name for name, _ in tools] + (["CipherCell SCA"] if sca is not None else []) + technologies = ([SCA] if sca is not None else []) + [name for name, _ in tools] return { "scan_id": str(sources.scan_id), @@ -277,7 +284,7 @@ def _summary( """The counts, and where they concentrate.""" sca_total = sca.total if sca else 0 raw = sum(report.total for _, report in tools) + sca_total - true_issues = [f"{len(_kept(report))} {name}" for name, report in tools] + ([f"{sca_total} SCA"] if sca else []) + true_issues = ([f"{sca_total} {SCA}"] if sca else []) + [f"{len(_kept(report))} {name}" for name, report in tools] summary = { "id": "summary", @@ -386,18 +393,30 @@ def _remedy(value: dict[str, Any]) -> str | None: def _issue_types(tools: list[tuple[str, SarifReport]], sca: VdrReport | None) -> list[str]: """How many distinct kinds of true issue each tool, and the dependency check, found.""" - counted = [f"{len({one.rule_id for one in _kept(report)})} {name} rules" for name, report in tools] + counted = [f"{sca.component_count} {SCA} affected components"] if sca is not None else [] - if sca is not None: - counted.append(f"{sca.component_count} affected components") - - return counted + return counted + [f"{len({one.rule_id for one in _kept(report)})} {name} rules" for name, report in tools] def _distributions(tools: list[tuple[str, SarifReport]], sca: VdrReport | None) -> list[dict[str, Any]]: - """Where the issues concentrate: by rule for each SARIF tool, by package for SCA.""" + """Where the issues concentrate: by package for SCA, by rule for each SARIF tool.""" blocks = [] + if sca and sca.vulnerabilities: + blocks.append( + { + "title": f"Most affected components, true {SCA} issues ({sca.component_count})", + **_bars( + _group( + [(one, name) for one in sca.vulnerabilities for name in one.component_names], + lambda pair: pair[1], + lambda pair: _vdr_severity(pair[0]), + ), + mono=True, + ), + } + ) + for name, report in tools: findings = _kept(report) @@ -409,21 +428,6 @@ def _distributions(tools: list[tuple[str, SarifReport]], sca: VdrReport | None) } ) - if sca and sca.vulnerabilities: - blocks.append( - { - "title": f"Most affected components, true SCA issues ({sca.component_count})", - **_bars( - _group( - [(one, name) for one in sca.vulnerabilities for name in one.component_names], - lambda pair: pair[1], - lambda pair: _vdr_severity(pair[0]), - ), - mono=True, - ), - } - ) - return blocks @@ -498,6 +502,23 @@ def _critical(tools: list[tuple[str, SarifReport]], sca: VdrReport | None) -> di """The handful worth reading first, whichever tool found them.""" rows: list[tuple[str, dict[str, Any]]] = [] + for vulnerability in sca.vulnerabilities if sca else []: + severity = _vdr_severity(vulnerability) + rows.append( + ( + severity, + { + "cells": [ + SCA, + {"value": severity, "kind": "severity"}, + _stated(vulnerability.id), + {"value": _stated(", ".join(vulnerability.component_names)), "kind": "mono"}, + _stated(vulnerability.description), + ] + }, + ) + ) + for name, report in tools: for finding in _kept(report): severity = _finding_severity(finding) @@ -516,24 +537,8 @@ def _critical(tools: list[tuple[str, SarifReport]], sca: VdrReport | None) -> di ) ) - for vulnerability in sca.vulnerabilities if sca else []: - severity = _vdr_severity(vulnerability) - rows.append( - ( - severity, - { - "cells": [ - "SCA", - {"value": severity, "kind": "severity"}, - _stated(vulnerability.id), - {"value": _stated(", ".join(vulnerability.component_names)), "kind": "mono"}, - _stated(vulnerability.description), - ] - }, - ) - ) - - # Sorted is stable, so within a severity each tool keeps its own order -- the rank, where it has one. + # Sorted is stable, so within a severity the sources keep the report's order, and each tool its own + # -- the rank, where it has one. rows.sort(key=lambda pair: SEVERITY_ORDER.index(pair[0])) return { @@ -634,7 +639,7 @@ def _sca_section(sca: VdrReport) -> dict[str, Any]: """The dependency findings.""" return { "id": "sca", - "title": "Dependency Vulnerabilities (SBOM / SCA)", + "title": f"{SCA} — Dependency Vulnerabilities", "new_page": True, "groups": [ {