feat(viewer): show confidence, CWE/CVE and the reasoning fields in the PDF report

The per-finding markdown report carries the CVE, CWE and confidence, plus the
counterevidence, the confidence rationale, what would change the severity and the
assumptions. The PDF rendered none of them: a reader of the PDF saw a CVSS score
and a severity but not how sure the agent was, what argued against the finding or
what would change its rating.

Add Confidence, CWE and CVE to the finding's header line, and the four reasoning
fields as blocks between the evidence and the remediation, in the same wording the
markdown report uses. Fields a finding does not have are skipped, like the existing
ones.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TavwVMH7PvrD68JTDfd6Hb
This commit is contained in:
Samir Hanna Verza 2026-10-06 19:42:18 -03:00
parent f1386cad37
commit 768b101615
2 changed files with 68 additions and 0 deletions

View file

@ -570,6 +570,16 @@ def _finding_flowables(
meta_bits = []
if vuln.get("cvss") is not None:
meta_bits.append(f"<b>CVSS</b> {_esc(vuln.get('cvss'))}")
confidence = str(vuln.get("confidence") or "").strip().title()
meta_bits.extend(
f"<b>{label}</b> {_esc(value)}"
for label, value in (
("Confidence", confidence),
("CWE", vuln.get("cwe")),
("CVE", vuln.get("cve")),
)
if value
)
meta_bits.extend(
f"<b>{key.title()}</b> {_esc(vuln.get(key))}"
for key in ("target", "endpoint", "method")
@ -592,6 +602,15 @@ def _finding_flowables(
poc_script = _strip_code_fence(vuln.get("poc_script_code"))
story.extend(_field_block(styles, "PoC script", poc_script, code=True))
story.extend(_field_block(styles, "Evidence", vuln.get("evidence"), code=True))
# What a reader needs to weigh the finding, as in the per-finding markdown report.
story.extend(_field_block(styles, "Counterevidence", vuln.get("counterevidence")))
story.extend(_field_block(styles, "Confidence rationale", vuln.get("confidence_rationale")))
story.extend(
_field_block(
styles, "What would change this severity", vuln.get("severity_change_conditions")
)
)
story.extend(_field_block(styles, "Assumptions", vuln.get("assumptions")))
remediation = vuln.get("remediation_steps")
if isinstance(remediation, list):

View file

@ -77,6 +77,55 @@ def test_generate_report_pdf_has_pdf_header(tmp_path: Path) -> None:
assert len(pdf) > 1000
def test_pdf_shows_what_a_reader_needs_to_weigh_a_finding(tmp_path: Path) -> None:
run_dir = _make_run(tmp_path)
vulns = json.loads((run_dir / "vulnerabilities.json").read_text(encoding="utf-8"))
vulns[0].update(
{
"confidence": "high",
"cwe": "CWE-89",
"cve": "CVE-2024-0001",
"counterevidence": "No WAF in front of the endpoint rewrote the payload.",
"confidence_rationale": "The injected sleep delayed the response three times.",
"severity_change_conditions": "Lower to high if the database user is read-only.",
"assumptions": "The staging database mirrors production.",
}
)
(run_dir / "vulnerabilities.json").write_text(json.dumps(vulns), encoding="utf-8")
text = " ".join(_pdf_text(generate_report_pdf(run_dir)).split())
for expected in (
"Confidence High",
"CWE CWE-89",
"CVE CVE-2024-0001",
"COUNTEREVIDENCE",
"No WAF in front of the endpoint rewrote the payload.",
"CONFIDENCE RATIONALE",
"The injected sleep delayed the response three times.",
"WHAT WOULD CHANGE THIS SEVERITY",
"Lower to high if the database user is read-only.",
"ASSUMPTIONS",
"The staging database mirrors production.",
):
assert expected in text
def test_pdf_leaves_out_the_trust_fields_a_finding_does_not_have(tmp_path: Path) -> None:
text = " ".join(_pdf_text(generate_report_pdf(_make_run(tmp_path))).split())
assert "SQL Injection" in text
for label in (
"Confidence",
"CWE",
"COUNTEREVIDENCE",
"CONFIDENCE RATIONALE",
"WHAT WOULD CHANGE THIS SEVERITY",
"ASSUMPTIONS",
):
assert label not in text
def test_generate_password_is_long_and_random() -> None:
first = generate_password()
second = generate_password()