mirror of
https://github.com/Strategic-Automation/violin.git
synced 2026-08-14 12:33:37 +02:00
217 lines
8.4 KiB
Python
217 lines
8.4 KiB
Python
"""Close-out gates: enforce the mandatory REPORTING / RETROSPECTIVE artifacts.
|
|
|
|
These are HARD gates (ERROR → exit 1). Unlike the advisory freshness warnings
|
|
in ``freshness.py`` (which are WARNING → exit 2 and *are* auto-approved under
|
|
``--yolo``), close-out violations map to ``denied`` even when
|
|
``HERMES_YOLO_MODE=1`` (see ``plugins/violin_guard/tools.py``). That is
|
|
deliberate: skipping REPORTING/RETROSPECTIVE is a skill-level violation — the
|
|
compliance audit found a prior engagement stopped at flag capture with no
|
|
report, no retrospective, no phase-summary, no CVSS vectors, and an empty
|
|
Research Log, because those artifacts were only ever emitted as yolo-approved
|
|
warnings.
|
|
|
|
Artifacts enforced (skill-defined paths):
|
|
- REPORTING : $ENG_DIR/reporting/report.md (non-trivial),
|
|
$ENG_DIR/state/phase-summary.md (transition summary),
|
|
CVSS:3.1 vector for any Critical/High finding,
|
|
non-empty hypotheses.md "Research Log" (RES- entries).
|
|
- RETROSPECTIVE: $ENG_DIR/retrospective.md (or evidence/retrospective/...),
|
|
phase-summary.md still present, CVSS still required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from guard.core import CheckResult
|
|
|
|
# Candidate locations for the mandated artifacts (skill-defined).
|
|
REPORT_CANDIDATES = [
|
|
"reporting/report.md",
|
|
"evidence/reporting/report.md",
|
|
"report.md",
|
|
]
|
|
RETRO_CANDIDATES = [
|
|
"retrospective.md",
|
|
"evidence/retrospective/retrospective.md",
|
|
"reporting/retrospective.md",
|
|
]
|
|
PHASE_SUMMARY = "state/phase-summary.md"
|
|
|
|
CVSS_RE = re.compile(r"CVSS:3\.1", re.IGNORECASE)
|
|
SEVERITY_RE = re.compile(r"(?i)severity\s*[:=]?\s*(critical|high)\b")
|
|
TIER_RE = re.compile(r"\b(L3|L4)\b")
|
|
|
|
# Commands that legitimately PRODUCE close-out artifacts, or are guard
|
|
# housekeeping — never blocked by the close-out gate, so the agent can create
|
|
# the very file the gate requires without deadlocking.
|
|
_REPORT_TOKENS = (
|
|
"report.md", "retrospective.md", "phase-summary.md",
|
|
"report-template", "coverage-matrix", "retrospective",
|
|
)
|
|
_WRITE_OPS = (
|
|
"write_file", "record-ptt", "record-hypothesis", "record-history",
|
|
"tee ", "cat >", "cat >>", "echo >", "echo >>", "printf >",
|
|
"sed -i", "vim ", "nano ",
|
|
)
|
|
_SAFE_META = (
|
|
"violin_guard.py", "hypothesis_guard.py", "sync-done", "heartbeat-done",
|
|
"message-tick", "check-command", "check-closeout", "check-bootstrap",
|
|
)
|
|
|
|
|
|
def _exists_nonempty(eng_dir: Path, rel: str, min_bytes: int) -> bool:
|
|
p = eng_dir / rel
|
|
if not p.exists() or not p.is_file():
|
|
return False
|
|
try:
|
|
text = p.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
return False
|
|
return text.strip() != "" and p.stat().st_size >= min_bytes
|
|
|
|
|
|
def _report_substantive(path: Path) -> bool:
|
|
text = path.read_text(encoding="utf-8", errors="replace").lower()
|
|
# Accept if it carries a CVSS vector (the key mandate) or a standard
|
|
# report section. Rejects empty/stub files.
|
|
if "cvss" in text:
|
|
return True
|
|
return any(tok in text for tok in ("severity", "finding", "summary", "methodology"))
|
|
|
|
|
|
def _report_ok(eng_dir: Path) -> bool:
|
|
for rel in REPORT_CANDIDATES:
|
|
if _exists_nonempty(eng_dir, rel, 50) and _report_substantive(eng_dir / rel):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _retro_ok(eng_dir: Path) -> bool:
|
|
for rel in RETRO_CANDIDATES:
|
|
if _exists_nonempty(eng_dir, rel, 30):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _phase_summary_ok(eng_dir: Path) -> bool:
|
|
return _exists_nonempty(eng_dir, PHASE_SUMMARY, 30)
|
|
|
|
|
|
def _cvss_ok(eng_dir: Path) -> bool:
|
|
"""True if no Critical/High/L3/L4 finding lacks a CVSS:3.1 vector."""
|
|
candidates = [
|
|
eng_dir / "evidence" / "vuln-research" / "findings.md",
|
|
eng_dir / "evidence" / "findings.md",
|
|
eng_dir / "state" / "findings.md",
|
|
eng_dir / "reporting" / "report.md",
|
|
eng_dir / "evidence" / "reporting" / "report.md",
|
|
]
|
|
found = [p for p in candidates if _exists_nonempty(p.parent, p.name, 1)]
|
|
if not found:
|
|
return True # no findings recorded -> nothing to score
|
|
combined = "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in found)
|
|
if CVSS_RE.search(combined):
|
|
return True
|
|
# No CVSS vector present: only a problem if a high-severity finding exists.
|
|
return not (SEVERITY_RE.search(combined) or TIER_RE.search(combined))
|
|
|
|
|
|
def _research_log_ok(hyp_path: Path) -> bool:
|
|
if not hyp_path.exists() or not hyp_path.is_file():
|
|
return False
|
|
text = hyp_path.read_text(encoding="utf-8", errors="replace")
|
|
m = re.search(r"##\s+Research Log", text)
|
|
if not m:
|
|
return False
|
|
tail = text[m.end():]
|
|
nxt = re.search(r"\n##\s+", tail)
|
|
section = tail[: nxt.start()] if nxt else tail
|
|
return bool(re.search(r"RES-\d+", section))
|
|
|
|
|
|
def _is_permitted(command: str) -> bool:
|
|
"""True when the command is producing a close-out artifact or is guard
|
|
housekeeping — so it must NOT be blocked by the close-out gate."""
|
|
c = command.lower()
|
|
if any(tok in c for tok in _REPORT_TOKENS):
|
|
return True
|
|
if any(op in c for op in _WRITE_OPS):
|
|
return True
|
|
if re.search(r">\s*\S*\.md(\b|$)", c):
|
|
return True
|
|
if any(meta in c for meta in _SAFE_META):
|
|
return True
|
|
return False
|
|
|
|
|
|
def check_closeout(eng_dir: str | Path, phase: str, command: str = "") -> CheckResult:
|
|
"""Hard gate for the mandatory close-out artifacts.
|
|
|
|
Returns a ``CheckResult`` whose ``.errors`` are non-empty (→ exit 1 →
|
|
``denied`` even under yolo) when a mandated artifact is missing *and* the
|
|
command is not the one producing it.
|
|
"""
|
|
result = CheckResult()
|
|
eng = Path(eng_dir)
|
|
phase = phase.upper().replace("-", "_")
|
|
permitted = _is_permitted(command or "")
|
|
|
|
if phase == "REPORTING":
|
|
# REPORTING is documentation-only. When the agent is running an
|
|
# artifact-producing command, accept it and re-check after save (no
|
|
# deadlock). Otherwise every missing artifact is a hard BLOCK.
|
|
if permitted:
|
|
result.add_info(
|
|
"close-out (REPORTING) command accepted — produce "
|
|
"state/phase-summary.md, reporting/report.md (with CVSS:3.1 "
|
|
"vectors for Critical/High findings), and a Research Log entry, "
|
|
"then re-run check-command."
|
|
)
|
|
else:
|
|
if not _phase_summary_ok(eng):
|
|
result.add_error(
|
|
"close-out gate: state/phase-summary.md missing/empty — write the "
|
|
"RECON→VULN-RESEARCH→EXPLOITATION→REPORTING transition summary before proceeding"
|
|
)
|
|
if not _report_ok(eng):
|
|
result.add_error(
|
|
"close-out gate: reporting/report.md not produced — REPORTING is "
|
|
"documentation-only; create the report before any other command"
|
|
)
|
|
if not _cvss_ok(eng):
|
|
result.add_error(
|
|
"close-out gate: a Critical/High (L3/L4) finding is present but no "
|
|
"CVSS:3.1 vector is recorded — add CVSS vectors before reporting"
|
|
)
|
|
if not _research_log_ok(eng / "hypotheses.md"):
|
|
result.add_error(
|
|
"close-out gate: hypotheses.md 'Research Log' has no RES- entries — "
|
|
"record the research loop (NVD/ExploitDB/…) before reporting"
|
|
)
|
|
|
|
elif phase == "RETROSPECTIVE":
|
|
if not _phase_summary_ok(eng):
|
|
result.add_error(
|
|
"close-out gate: state/phase-summary.md missing/empty for retrospective"
|
|
)
|
|
if not _retro_ok(eng):
|
|
if permitted:
|
|
result.add_info("retrospective-production command accepted; re-run check-command after saving")
|
|
else:
|
|
result.add_error(
|
|
"close-out gate: retrospective.md not produced — RETROSPECTIVE is "
|
|
"mandatory after every engagement"
|
|
)
|
|
if not _report_ok(eng):
|
|
result.add_error(
|
|
"close-out gate: reporting/report.md missing — complete REPORTING before RETROSPECTIVE"
|
|
)
|
|
if not _cvss_ok(eng):
|
|
result.add_error(
|
|
"close-out gate: a Critical/High (L3/L4) finding lacks a CVSS:3.1 vector"
|
|
)
|
|
|
|
return result
|