Report CipherCell SCA first, then Lynkeus, then SAST, everywhere

The dependency check is named CipherCell SCA throughout, and the sources
keep one order on the cover, in the summary, the most-critical table and
the sections. Lynkeus before SAST no longer depends on the run order in
the SARIF file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alexanderandClaude Opus 5.5 committed 2026-09-24 21:31:37 +02:00
1 parent cce3e85e9d
commit 303bcdf0b4
2 files changed
+55 -49

No files matched your search

+3 -2
View File
@@ -45,8 +45,9 @@ project page is left out. The report has these parts:
1. the cover and a table of contents, 1. the cover and a table of contents,
2. a summary and the most critical issues across all sources, 2. a summary and the most critical issues across all sources,
3. the dependency vulnerabilities from the VDR, 3. the dependency vulnerabilities from the VDR, as CipherCell SCA,
4. one section per SARIF run, named after its tool (e.g. CipherCell Lynkeus, CipherCell SAST), 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 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 false positive or that carries a SARIF suppression (such as the areas Lynkeus ruled out), with
the reason. the reason.
+52 -47
View File
@@ -44,6 +44,13 @@ BARS = 10
SEVERITY_ORDER = ("Critical", "High", "Medium", "Low") 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. # A full git commit SHA.
_COMMIT = re.compile(r"[0-9a-f]{40}") _COMMIT = re.compile(r"[0-9a-f]{40}")
@@ -77,7 +84,8 @@ def build_context(
context: dict[str, Any] = { context: dict[str, Any] = {
"cover": _cover(sources, metadata, tools, sca, project=project, manufacturer=manufacturer), "cover": _cover(sources, metadata, tools, sca, project=project, manufacturer=manufacturer),
"summary": _summary(tools, sca, documents.get("metrics")), "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. # 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"]: if critical["table"]["rows"]:
context["critical"] = critical context["critical"] = critical
if sca is not None:
context["sections"].insert(0, _sca_section(sca))
set_aside = _set_aside_section(tools) set_aside = _set_aside_section(tools)
if set_aside is not None: 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)] 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: def _summarize(run: dict[str, Any]) -> SarifReport:
@@ -182,7 +189,7 @@ def _cover(
manufacturer: str | None, manufacturer: str | None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""The first page.""" """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 { return {
"scan_id": str(sources.scan_id), "scan_id": str(sources.scan_id),
@@ -277,7 +284,7 @@ def _summary(
"""The counts, and where they concentrate.""" """The counts, and where they concentrate."""
sca_total = sca.total if sca else 0 sca_total = sca.total if sca else 0
raw = sum(report.total for _, report in tools) + sca_total 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 = { summary = {
"id": "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]: 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.""" """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: return counted + [f"{len({one.rule_id for one in _kept(report)})} {name} rules" for name, report in tools]
counted.append(f"{sca.component_count} affected components")
return counted
def _distributions(tools: list[tuple[str, SarifReport]], sca: VdrReport | None) -> list[dict[str, Any]]: 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 = [] 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: for name, report in tools:
findings = _kept(report) 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 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.""" """The handful worth reading first, whichever tool found them."""
rows: list[tuple[str, dict[str, Any]]] = [] 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 name, report in tools:
for finding in _kept(report): for finding in _kept(report):
severity = _finding_severity(finding) 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 []: # Sorted is stable, so within a severity the sources keep the report's order, and each tool its own
severity = _vdr_severity(vulnerability) # -- the rank, where it has one.
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.
rows.sort(key=lambda pair: SEVERITY_ORDER.index(pair[0])) rows.sort(key=lambda pair: SEVERITY_ORDER.index(pair[0]))
return { return {
@@ -634,7 +639,7 @@ def _sca_section(sca: VdrReport) -> dict[str, Any]:
"""The dependency findings.""" """The dependency findings."""
return { return {
"id": "sca", "id": "sca",
"title": "Dependency Vulnerabilities (SBOM / SCA)", "title": f"{SCA} — Dependency Vulnerabilities",
"new_page": True, "new_page": True,
"groups": [ "groups": [
{ {