Make write_executive_report atomic to avoid destroying prior report

write_executive_report opened the destination with path.open("w"),
which truncates the file the moment it's opened, before any content
is written. Every other writer in this module already goes through
_atomic_write_text, which writes to a temp file and renames it into
place. Align write_executive_report with the same pattern so a failed
write can no longer destroy the previous penetration_test_report.md.

Closes #828.
This commit is contained in:
mmaxjr 2026-07-31 15:08:37 -03:00
parent a9deb84260
commit 4756108ef0
2 changed files with 24 additions and 4 deletions

View file

@ -115,10 +115,12 @@ def write_run_record(run_dir: Path, run_record: dict[str, Any]) -> None:
def write_executive_report(run_dir: Path, final_scan_result: str) -> None:
path = run_dir / "penetration_test_report.md"
with path.open("w", encoding="utf-8") as f:
f.write("# Security Penetration Test Report\n\n")
f.write(f"**Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n")
f.write(f"{final_scan_result}\n")
content = (
"# Security Penetration Test Report\n\n"
f"**Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n"
f"{final_scan_result}\n"
)
_atomic_write_text(path, content)
logger.info("Saved final penetration test report to: %s", path)

View file

@ -179,3 +179,21 @@ def test_write_executive_report_writes_markdown(tmp_path: Path) -> None:
content = (tmp_path / "penetration_test_report.md").read_text(encoding="utf-8")
assert "# Security Penetration Test Report" in content
assert "Scan complete. No critical issues." in content
def test_write_executive_report_preserves_previous_report_on_failure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
report_path = tmp_path / "penetration_test_report.md"
report_path.write_text("previous report", encoding="utf-8")
def boom(*_args: Any, **_kwargs: Any) -> None:
raise OSError("disk full")
monkeypatch.setattr("strix.report.writer.tempfile.NamedTemporaryFile", boom)
with pytest.raises(OSError, match="disk full"):
write_executive_report(tmp_path, "new content")
assert report_path.read_text(encoding="utf-8") == "previous report"