Add audited response apply rehearsal
This commit is contained in:
parent
51d52b5229
commit
0b010df514
11 changed files with 198 additions and 28 deletions
|
|
@ -312,6 +312,69 @@ def persist_plan(cfg: Config, plan: dict, actor: str = "cli") -> dict[str, str]:
|
|||
return {"plan_path": str(plan_path), "audit_log": str(audit_path)}
|
||||
|
||||
|
||||
def load_persisted_plan(cfg: Config, ref: str) -> tuple[dict, Path] | tuple[None, None]:
|
||||
"""Load a persisted response plan by path, filename, or plan_id.
|
||||
|
||||
``respond apply`` deliberately consumes a saved artifact instead of a freshly
|
||||
generated preview, so the operator is applying exactly what was reviewed.
|
||||
"""
|
||||
candidates: list[Path] = []
|
||||
raw = Path(ref)
|
||||
if raw.is_file():
|
||||
candidates.append(raw)
|
||||
plan_dir = cfg.log_dir / "response-plans"
|
||||
if not raw.is_absolute():
|
||||
candidates.extend([
|
||||
plan_dir / ref,
|
||||
plan_dir / f"{ref}.json",
|
||||
])
|
||||
try:
|
||||
candidates.extend(sorted(plan_dir.glob("*.json"), reverse=True))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
seen = set()
|
||||
for path in candidates:
|
||||
if path in seen or not path.is_file():
|
||||
continue
|
||||
seen.add(path)
|
||||
try:
|
||||
plan = json.loads(path.read_text())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if path == raw or path.name == ref or plan.get("plan_id") == ref:
|
||||
return plan, path
|
||||
return None, None
|
||||
|
||||
|
||||
def rehearse_apply(cfg: Config, plan: dict, plan_path: Path,
|
||||
actor: str = "cli") -> dict:
|
||||
"""Audit an explicit apply rehearsal without executing plan commands."""
|
||||
record = {
|
||||
"schema": schemas.RESPONSE_AUDIT_V1,
|
||||
"time": datetime.now().astimezone().isoformat(),
|
||||
"event": "response_apply_rehearsed",
|
||||
"actor": actor,
|
||||
"plan_id": plan.get("plan_id"),
|
||||
"incident_id": plan.get("incident_id"),
|
||||
"mode": plan.get("mode"),
|
||||
"apply_supported": plan.get("apply_supported", False),
|
||||
"action_count": len(plan.get("actions", [])),
|
||||
"plan_path": str(plan_path),
|
||||
"executed": False,
|
||||
"reason": "dry-run rehearsal; no commands executed",
|
||||
}
|
||||
audit_path = response_audit_path(cfg)
|
||||
cfg.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
with audit_path.open("a") as fh:
|
||||
fh.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
try:
|
||||
audit_path.chmod(0o640)
|
||||
except OSError:
|
||||
pass
|
||||
return {"audit_log": str(audit_path), "record": record}
|
||||
|
||||
|
||||
def response_audit_path(cfg: Config) -> Path:
|
||||
return cfg.log_dir / "response-audit.log"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue