Add audited response apply rehearsal
This commit is contained in:
parent
51d52b5229
commit
0b010df514
11 changed files with 198 additions and 28 deletions
|
|
@ -237,11 +237,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||
help="posture action (default: check)")
|
||||
po.add_argument("--json", action="store_true", help="emit findings as JSON")
|
||||
rsp = sub.add_parser("respond",
|
||||
help="build dry-run response plans for incidents")
|
||||
rsp.add_argument("action", nargs="?", default="plan", choices=["plan"],
|
||||
help="build and rehearse response plans for incidents")
|
||||
rsp.add_argument("action", nargs="?", default="plan", choices=["plan", "apply"],
|
||||
help="response action (default: plan)")
|
||||
rsp.add_argument("id", nargs="?", help="incident id")
|
||||
rsp.add_argument("--json", action="store_true", help="emit plan as JSON")
|
||||
rsp.add_argument("id", nargs="?",
|
||||
help="incident id for plan, or saved plan path/id for apply")
|
||||
rsp.add_argument("--json", action="store_true", help="emit JSON")
|
||||
rsp.add_argument("--dry-run", action="store_true",
|
||||
help="for respond apply, audit and print actions without executing")
|
||||
wd = sub.add_parser("watchdog",
|
||||
help="poll a remote dashboard and push if Sentinel is silent")
|
||||
wd.add_argument("--url", required=True, help="dashboard base URL")
|
||||
|
|
@ -305,7 +308,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
if args.cmd == "posture":
|
||||
return _cmd_posture(cfg, args.json)
|
||||
if args.cmd == "respond":
|
||||
return _cmd_respond(cfg, args.action, args.id, args.json)
|
||||
return _cmd_respond(cfg, args.action, args.id, args.json, args.dry_run)
|
||||
if args.cmd == "watchdog":
|
||||
return _cmd_watchdog(cfg, args.url, args.token, args.max_age,
|
||||
not args.insecure_tls)
|
||||
|
|
@ -581,14 +584,14 @@ def _cmd_posture(cfg: Config, as_json: bool) -> int:
|
|||
return 1
|
||||
|
||||
|
||||
def _cmd_respond(cfg: Config, action: str, iid: str | None, as_json: bool) -> int:
|
||||
def _cmd_respond(cfg: Config, action: str, iid: str | None, as_json: bool,
|
||||
dry_run: bool = False) -> int:
|
||||
import json
|
||||
|
||||
from . import respond
|
||||
|
||||
if action != "plan":
|
||||
print(f"error: unsupported respond action: {action}", file=sys.stderr)
|
||||
return 2
|
||||
if action == "apply":
|
||||
return _cmd_respond_apply(cfg, iid, as_json, dry_run)
|
||||
if not iid:
|
||||
print("error: 'respond plan' needs an incident id "
|
||||
"(see 'incident list')", file=sys.stderr)
|
||||
|
|
@ -622,6 +625,56 @@ def _cmd_respond(cfg: Config, action: str, iid: str | None, as_json: bool) -> in
|
|||
return 0
|
||||
|
||||
|
||||
def _cmd_respond_apply(cfg: Config, ref: str | None, as_json: bool,
|
||||
dry_run: bool) -> int:
|
||||
import json
|
||||
|
||||
from . import respond
|
||||
|
||||
if not ref:
|
||||
print("error: 'respond apply' needs a saved plan path or plan id",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
if not dry_run:
|
||||
print("error: response apply execution is not implemented; rerun with "
|
||||
"--dry-run to rehearse and audit the reviewed plan",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
plan, plan_path = respond.load_persisted_plan(cfg, ref)
|
||||
if plan is None or plan_path is None:
|
||||
print(f"error: no such response plan: {ref}", file=sys.stderr)
|
||||
return 1
|
||||
result = respond.rehearse_apply(cfg, plan, plan_path)
|
||||
output = {
|
||||
"schema": schemas.RESPONSE_AUDIT_V1,
|
||||
"plan": {
|
||||
"plan_id": plan.get("plan_id"),
|
||||
"incident_id": plan.get("incident_id"),
|
||||
"plan_path": str(plan_path),
|
||||
"action_count": len(plan.get("actions", [])),
|
||||
},
|
||||
"audit": result["record"],
|
||||
"audit_log": result["audit_log"],
|
||||
"executed": False,
|
||||
}
|
||||
if as_json:
|
||||
print(json.dumps(output, indent=2))
|
||||
return 0
|
||||
|
||||
print(f"Response apply rehearsal {plan.get('plan_id', ref)}")
|
||||
print(f" incident: {plan.get('incident_id', '?')}")
|
||||
print(f" plan: {plan_path}")
|
||||
print(f" audit: {result['audit_log']}")
|
||||
print(" executed: no")
|
||||
print(" actions:")
|
||||
for action in plan.get("actions", []):
|
||||
cmd = " ".join(action.get("command", []))
|
||||
print(f" {action.get('id', '?')} [{action.get('risk', '?')}] "
|
||||
f"{action.get('title', '')}")
|
||||
print(f" {cmd}")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_fim_check(cfg: Config, packages: bool) -> int:
|
||||
from . import fim, reconcile
|
||||
sentinel = Sentinel(cfg)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue