diff --git a/strix/interface/viewer/report_pdf.py b/strix/interface/viewer/report_pdf.py
index b749f270..a4165094 100644
--- a/strix/interface/viewer/report_pdf.py
+++ b/strix/interface/viewer/report_pdf.py
@@ -570,6 +570,16 @@ def _finding_flowables(
meta_bits = []
if vuln.get("cvss") is not None:
meta_bits.append(f"CVSS {_esc(vuln.get('cvss'))}")
+ confidence = str(vuln.get("confidence") or "").strip().title()
+ meta_bits.extend(
+ f"{label} {_esc(value)}"
+ for label, value in (
+ ("Confidence", confidence),
+ ("CWE", vuln.get("cwe")),
+ ("CVE", vuln.get("cve")),
+ )
+ if value
+ )
meta_bits.extend(
f"{key.title()} {_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):
diff --git a/tests/test_report_pdf.py b/tests/test_report_pdf.py
index 61d1f205..c68a3f59 100644
--- a/tests/test_report_pdf.py
+++ b/tests/test_report_pdf.py
@@ -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()