from __future__ import annotations
import argparse
import hashlib
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRATCH_REPORT = ROOT / "PENTEST.md"
COMMIT_HASH_PATTERN = r"^[0-9a-fA-F]{40}$"
def capture(command: list[str]) -> str:
return subprocess.check_output(command, cwd=ROOT, text=True).strip()
def workspace_version() -> str:
cargo_toml = ROOT / "Cargo.toml"
for line in cargo_toml.read_text(encoding="utf-8").splitlines():
if line.startswith("version = "):
return line.split('"', 2)[1]
raise RuntimeError("could not determine workspace version")
def sha256_digest(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
def validate_report_arg(name: str, value: str, pattern: str = r"^[^\n\r]+$") -> str:
if re.fullmatch(pattern, value) is None:
raise ValueError(f"--{name} contains invalid characters: {value!r}")
return value
def build_report(
*,
version: str,
status: str,
tester: str,
scope: str,
date: str,
commit: str,
input_digest: str,
scratch_text: str,
) -> str:
tag = f"v{version}"
return f"""# BCX {tag} Pentest Report
Tag: {tag}
Commit: {commit}
Status: {status}
Tester: {tester}
Date: {date}
Scope: {scope}
## Summary
This report records the release pentest result for BCX `{tag}`. The root
`PENTEST.md` scratch input was reviewed, remediated where release-scope fixes
were required, and then captured here by digest before deletion.
Input-Digest: sha256:{input_digest}
## Scratch Report
```text
{scratch_text.rstrip()}
```
"""
def main() -> int:
parser = argparse.ArgumentParser(
description="Record root PENTEST.md under security/pentest/."
)
parser.add_argument(
"--version",
default=workspace_version(),
help="Release version without leading v. Defaults to workspace version.",
)
parser.add_argument("--status", default="PASS", help="Pentest status.")
parser.add_argument(
"--tester",
required=True,
help="Tester or review role to record in the permanent report.",
)
parser.add_argument("--scope", required=True, help="Pentest scope.")
parser.add_argument(
"--date",
required=True,
help="Pentest date in YYYY-MM-DD format.",
)
parser.add_argument(
"--commit",
default=capture(["git", "rev-parse", "HEAD"]),
help="Exact 40-character commit hash that was pentested. Defaults to HEAD.",
)
parser.add_argument(
"--output",
help="Override output path. Defaults to security/pentest/v<version>.md.",
)
args = parser.parse_args()
if not SCRATCH_REPORT.is_file():
print("missing root PENTEST.md scratch report", file=sys.stderr)
return 1
tester = validate_report_arg("tester", args.tester)
scope = validate_report_arg("scope", args.scope)
date = validate_report_arg("date", args.date, r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
status = validate_report_arg("status", args.status, r"^[A-Z]+$")
commit = validate_report_arg("commit", args.commit, COMMIT_HASH_PATTERN)
scratch_bytes = SCRATCH_REPORT.read_bytes()
scratch_text = scratch_bytes.decode("utf-8")
input_digest = sha256_digest(scratch_bytes)
output = (
ROOT / args.output
if args.output
else ROOT / "security" / "pentest" / f"v{args.version}.md"
)
report = build_report(
version=args.version,
status=status,
tester=tester,
scope=scope,
date=date,
commit=commit,
input_digest=input_digest,
scratch_text=scratch_text,
)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(report, encoding="utf-8")
print(f"wrote {display_path(output)}")
print(f"Input-Digest: sha256:{input_digest}")
print("Delete root PENTEST.md after reviewing the permanent report.")
return 0
if __name__ == "__main__":
raise SystemExit(main())