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()