Describe the bug
write_executive_report (strix/report/writer.py:58) is the only artifact writer in the module that does not go through the module's own _atomic_write_text helper. It opens the destination with path.open("w"), which truncates penetration_test_report.md in place before any new bytes are written:
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")
Every sibling writer in the same file is already atomic — write_run_record (:52), the per-vulnerability markdown (:78), vulnerabilities.csv (:103) and vulnerabilities.json (:105) all call _atomic_write_text, which writes to a temp file in the same directory and Path.replace()s it into place.
So if anything raises or the process dies between the open("w") and the final f.write(...) — ENOSPC, a disconnected network mount, Ctrl-C, an OOM kill — the run ends with a truncated or empty penetration_test_report.md, and the previous complete report is already gone. On a re-run of a long scan that is the primary human-readable deliverable, lost with no copy.
To Reproduce
- Complete a scan so
penetration_test_report.md exists with real content.
- Start a second write and have it fail partway through, e.g. fill the volume (ENOSPC) or interrupt between the two
f.write calls.
- Read
penetration_test_report.md.
A deterministic version of the same failure, no full scan needed:
from unittest.mock import patch
from strix.report import writer
writer.write_executive_report(tmp_path, "First complete report.")
class Boom:
@staticmethod
def now(_tz):
raise OSError("clock unavailable")
with patch.object(writer, "datetime", Boom):
try:
writer.write_executive_report(tmp_path, "Second report.")
except OSError:
pass
print((tmp_path / "penetration_test_report.md").read_text())
Expected behavior
A failed write leaves the previously written report intact, matching the atomicity guarantee every other writer in writer.py already provides.
Actual behavior
The file is left containing only # Security Penetration Test Report\n\n (the header written before the failure). The prior complete report is unrecoverable.
System Information:
- OS: macOS 15 (the bug is filesystem/OS independent)
- Strix Version or Commit:
48b4821 (v1.2.0)
- Python Version: 3.12
- LLM Used: n/a — reproduces without an LLM call
Additional context
Fix is a two-line change: build the payload first, then hand it to the existing _atomic_write_text. I have a PR ready with a regression test that fails on main and passes with the fix.
Describe the bug
write_executive_report(strix/report/writer.py:58) is the only artifact writer in the module that does not go through the module's own_atomic_write_texthelper. It opens the destination withpath.open("w"), which truncatespenetration_test_report.mdin place before any new bytes are written:Every sibling writer in the same file is already atomic —
write_run_record(:52), the per-vulnerability markdown (:78),vulnerabilities.csv(:103) andvulnerabilities.json(:105) all call_atomic_write_text, which writes to a temp file in the same directory andPath.replace()s it into place.So if anything raises or the process dies between the
open("w")and the finalf.write(...)— ENOSPC, a disconnected network mount,Ctrl-C, an OOM kill — the run ends with a truncated or emptypenetration_test_report.md, and the previous complete report is already gone. On a re-run of a long scan that is the primary human-readable deliverable, lost with no copy.To Reproduce
penetration_test_report.mdexists with real content.f.writecalls.penetration_test_report.md.A deterministic version of the same failure, no full scan needed:
Expected behavior
A failed write leaves the previously written report intact, matching the atomicity guarantee every other writer in
writer.pyalready provides.Actual behavior
The file is left containing only
# Security Penetration Test Report\n\n(the header written before the failure). The prior complete report is unrecoverable.System Information:
48b4821(v1.2.0)Additional context
Fix is a two-line change: build the payload first, then hand it to the existing
_atomic_write_text. I have a PR ready with a regression test that fails onmainand passes with the fix.