From 09347a7ec2197a65ceb783434bb5b234ed09eca5 Mon Sep 17 00:00:00 2001 From: Alex Schapiro Date: Mon, 31 Aug 2026 03:22:27 +0000 Subject: [PATCH] report: normalize hydrated titles on resume --- strix/report/state.py | 3 +++ tests/test_list_reports.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/strix/report/state.py b/strix/report/state.py index e043f088..594925ba 100644 --- a/strix/report/state.py +++ b/strix/report/state.py @@ -236,6 +236,9 @@ class ReportState: ) self.vulnerability_reports = [r for r in data if isinstance(r, dict)] for r in self.vulnerability_reports: + title = r.get("title") + if isinstance(title, str): + r["title"] = _clean_title(title) rid = r.get("id") if isinstance(rid, str): self._saved_vuln_ids.add(rid) diff --git a/tests/test_list_reports.py b/tests/test_list_reports.py index 8fb90f9d..373439cd 100644 --- a/tests/test_list_reports.py +++ b/tests/test_list_reports.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING import pytest @@ -41,6 +42,21 @@ def test_add_vulnerability_report_strips_control_chars_from_title( assert report["title"] == "XSS in search form" +def test_hydrate_from_run_dir_strips_control_chars_from_title( + report_state: ReportState, +) -> None: + # A run started before titles were normalized can hold control characters on + # disk, and resume re-exports those titles to the CSV, the SARIF and the TUI. + (report_state.get_run_dir() / "vulnerabilities.json").write_text( + json.dumps([{"id": "vuln-0001", "title": "XSS in\r\n search\tform", "severity": "medium"}]), + encoding="utf-8", + ) + + report_state.hydrate_from_run_dir() + + assert report_state.vulnerability_reports[0]["title"] == "XSS in search form" + + def _seed(state: ReportState) -> None: state.add_vulnerability_report( title="Reflected XSS in search",