diff --git a/AINative_OKR_CASAN5/.specify/scripts/bash/casan-harness.sh b/AINative_OKR_CASAN5/.specify/scripts/bash/casan-harness.sh index a93bae9..65b19a2 100755 --- a/AINative_OKR_CASAN5/.specify/scripts/bash/casan-harness.sh +++ b/AINative_OKR_CASAN5/.specify/scripts/bash/casan-harness.sh @@ -82,6 +82,20 @@ APPROVED_INPUT="$TMP_DIR/governance-approved-$TRACE_SUFFIX.txt" RAW_OUTPUT="$TMP_DIR/raw-output-$TRACE_SUFFIX.txt" casan_log debug harness "action=$ACTION_NAME input=$INPUT_FILE output=$FINAL_OUTPUT key=${IDEMPOTENCY_KEY:0:12}…" + +# C7: honor an engaged kill-switch before doing any work (incident containment). +# Opt-in (default off) so the baseline is unchanged; production sets it on. +if [[ "${CASAN_KILLSWITCH_ENFORCE:-0}" == "1" ]]; then + KS_SCOPE="${CASAN_KILLSWITCH_SCOPE:-project}" + KS_ID="${CASAN_KILLSWITCH_ID:-${CASAN_PROJECT:-current}}" + if ! bash "$SCRIPT_DIR/kill-switch.sh" check "$KS_SCOPE" "$KS_ID" >/dev/null 2>&1; then + casan_log error harness "KILL_SWITCH_ACTIVE scope=$KS_SCOPE id=$KS_ID — refusing to run $ACTION_NAME" + : > "$FINAL_OUTPUT" + echo "KILL_SWITCH_ACTIVE scope=$KS_SCOPE id=$KS_ID action=$ACTION_NAME" >&2 + exit 2 + fi +fi + run_phase "H4-in" "$SCRIPT_DIR/security-check.sh" "$INPUT_FILE" "$SAFE_INPUT" input run_phase "H5" "$SCRIPT_DIR/governance-check.sh" "$SAFE_INPUT" "$APPROVED_INPUT" "$ACTION_NAME" diff --git a/AINative_OKR_CASAN5/.specify/scripts/bash/incident.sh b/AINative_OKR_CASAN5/.specify/scripts/bash/incident.sh new file mode 100755 index 0000000..c23c487 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/scripts/bash/incident.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -uo pipefail + +# CASAN — Incident response (C7 / V23). +# +# Turns a detected security/ops event into a graded incident: classify severity, +# record a tamper-visible incident entry, and for HIGH/CRIT auto-engage the +# scoped kill-switch + fire an alert (reuses alert-dispatch.sh from H6 if present). +# Answers "when a gate catches an attack/spike/tamper — who is paged and what +# stops?" — severity, owner, kill-switch, runbook. +# +# Usage: +# incident.sh raise [detail] [--scope ] [--id ] +# incident.sh status +# Exit: 0 recorded (LOW/MED) · 2 kill-switch engaged (HIGH/CRIT) · 64 usage. + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SEC_DIR="$PROJECT_ROOT/.specify/security" +LOG="$PROJECT_ROOT/.specify/logs/level5/incidents.jsonl" +RUNBOOK="$SEC_DIR/incident-runbook.md" +SEVMAP="$SEC_DIR/incident-severity.map" +mkdir -p "$(dirname "$LOG")" +# shellcheck source=casan-log.sh +source "$SCRIPT_DIR/casan-log.sh" + +CMD="${1:-}" +ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } + +# owner routing by severity (production: on-call rota / IdP group). +owner_for() { case "$1" in CRIT) echo "security-oncall" ;; HIGH) echo "ops-oncall" ;; MED) echo "tech-lead" ;; *) echo "triage" ;; esac; } + +if [[ "$CMD" == "status" ]]; then + n=$(grep -c . "$LOG" 2>/dev/null || echo 0) + echo "INCIDENTS total=$n log=$LOG" + [[ -f "$LOG" ]] && tail -5 "$LOG" + exit 0 +fi +[[ "$CMD" == "raise" ]] || { echo "Usage: incident.sh raise [detail] [--scope ] [--id ]" >&2; exit 64; } + +EVENT="${2:-}"; DETAIL="${3:-}" +[[ -n "$EVENT" ]] || { echo "usage: incident.sh raise [detail]" >&2; exit 64; } +SCOPE="project"; ID="${CASAN_PROJECT:-current}" +shift 2 2>/dev/null || true +while [[ "$#" -gt 0 ]]; do + case "$1" in + --scope) SCOPE="${2:-project}"; shift 2 ;; + --id) ID="${2:-current}"; shift 2 ;; + *) shift ;; + esac +done + +# Classify severity from the map (fallback to default). +SEV="$(awk -v e="$EVENT" '$1==e {print $2; exit}' "$SEVMAP" 2>/dev/null)" +[[ -n "$SEV" ]] || SEV="$(awk '$1=="default" {print $2; exit}' "$SEVMAP" 2>/dev/null)" +[[ -n "$SEV" ]] || SEV="MED" +OWNER="$(owner_for "$SEV")" +TS="$(ts)" +ACTION="recorded" + +# HIGH/CRIT → engage the scoped kill-switch (stop the blast radius). +if [[ "$SEV" == "CRIT" || "$SEV" == "HIGH" ]]; then + bash "$SCRIPT_DIR/kill-switch.sh" engage "$SCOPE" "$ID" "incident:$EVENT" >/dev/null 2>&1 || true + ACTION="kill_switch_engaged" + # Fire an alert through the H6 dispatcher if it is wired up. + if [[ -x "$SCRIPT_DIR/alert-dispatch.sh" ]]; then + bash "$SCRIPT_DIR/alert-dispatch.sh" "$SEV" "incident:$EVENT" "$DETAIL" >/dev/null 2>&1 || true + fi + casan_log error incident "INCIDENT sev=$SEV event=$EVENT scope=$SCOPE id=$ID → kill-switch ENGAGED owner=$OWNER" +else + casan_log warn incident "INCIDENT sev=$SEV event=$EVENT scope=$SCOPE id=$ID owner=$OWNER" +fi + +# Record a structured incident entry. +python - "$LOG" "$TS" "$EVENT" "$SEV" "$OWNER" "$SCOPE" "$ID" "$ACTION" "$DETAIL" "$RUNBOOK" <<'PY' 2>/dev/null || \ + printf '{"timestamp":"%s","event":"%s","severity":"%s","owner":"%s","scope":"%s","id":"%s","action":"%s"}\n' \ + "$TS" "$EVENT" "$SEV" "$OWNER" "$SCOPE" "$ID" "$ACTION" >> "$LOG" +import json, sys +log, ts, event, sev, owner, scope, iid, action, detail, runbook = sys.argv[1:11] +with open(log, "a", encoding="utf-8") as f: + f.write(json.dumps({ + "timestamp": ts, "event": event, "severity": sev, "owner": owner, + "scope": scope, "id": iid, "action": action, "detail": detail[:300], + "runbook": runbook, + }) + "\n") +PY + +echo "INCIDENT_RAISED sev=$SEV event=$EVENT owner=$OWNER scope=$SCOPE id=$ID action=$ACTION runbook=$RUNBOOK" +[[ "$SEV" == "CRIT" || "$SEV" == "HIGH" ]] && exit 2 || exit 0 diff --git a/AINative_OKR_CASAN5/.specify/scripts/bash/kill-switch.sh b/AINative_OKR_CASAN5/.specify/scripts/bash/kill-switch.sh new file mode 100755 index 0000000..9120269 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/scripts/bash/kill-switch.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -uo pipefail + +# CASAN H6/H7 — Kill-switch (Incident response · C7 / V23). +# +# A scoped emergency stop: engage a switch for a project / model / provider and +# any gate that honors it refuses to run further work in that scope. Engaging is +# recorded; clearing requires an explicit reason (production: reviewer approval). +# +# Usage: +# kill-switch.sh engage [reason] # turn the switch ON +# kill-switch.sh clear [reason] # turn it OFF (audited) +# kill-switch.sh check # exit 2 if engaged, 0 if clear +# kill-switch.sh status # list engaged switches +# scope ∈ {project, model, provider, global}. A `global` switch stops everything. +# Env: CASAN_KILLSWITCH_DIR (default .specify/logs/level5/kill-switch) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +KS_DIR="${CASAN_KILLSWITCH_DIR:-$PROJECT_ROOT/.specify/logs/level5/kill-switch}" +mkdir -p "$KS_DIR" + +CMD="${1:-}"; SCOPE="${2:-}"; ID="${3:-}"; REASON="${4:-unspecified}" +ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } +safe() { printf '%s' "$1" | tr '/ :' '___'; } + +case "$CMD" in + engage) + [[ -n "$SCOPE" && -n "$ID" ]] || { echo "usage: kill-switch.sh engage [reason]" >&2; exit 64; } + f="$KS_DIR/$(safe "$SCOPE")-$(safe "$ID").on" + printf '{"scope":"%s","id":"%s","reason":"%s","engaged_at":"%s","actor":"%s"}\n' \ + "$SCOPE" "$ID" "$REASON" "$(ts)" "${CASAN_ACTOR:-system}" > "$f" + echo "KILL_SWITCH_ENGAGED scope=$SCOPE id=$ID reason=$REASON" + ;; + clear) + [[ -n "$SCOPE" && -n "$ID" ]] || { echo "usage: kill-switch.sh clear [reason]" >&2; exit 64; } + f="$KS_DIR/$(safe "$SCOPE")-$(safe "$ID").on" + if [[ -f "$f" ]]; then + printf '%s cleared_by=%s reason=%s at=%s\n' "$(cat "$f")" "${CASAN_ACTOR:-system}" "$REASON" "$(ts)" \ + >> "$KS_DIR/kill-switch-history.log" + rm -f "$f" + echo "KILL_SWITCH_CLEARED scope=$SCOPE id=$ID" + else + echo "KILL_SWITCH_NOT_ENGAGED scope=$SCOPE id=$ID" + fi + ;; + check) + [[ -n "$SCOPE" && -n "$ID" ]] || { echo "usage: kill-switch.sh check " >&2; exit 64; } + # A global switch, or a switch for this exact scope/id, blocks. + if [[ -f "$KS_DIR/global-all.on" ]]; then + echo "KILL_SWITCH_ACTIVE scope=global" >&2; exit 2 + fi + if [[ -f "$KS_DIR/$(safe "$SCOPE")-$(safe "$ID").on" ]]; then + echo "KILL_SWITCH_ACTIVE scope=$SCOPE id=$ID" >&2; exit 2 + fi + echo "KILL_SWITCH_CLEAR scope=$SCOPE id=$ID"; exit 0 + ;; + status) + n=0 + for f in "$KS_DIR"/*.on; do [[ -e "$f" ]] || continue; cat "$f"; n=$((n+1)); done + echo "KILL_SWITCH_STATUS engaged=$n" + ;; + *) + echo "Usage: kill-switch.sh {engage|clear|check|status} [reason]" >&2 + exit 64 ;; +esac diff --git a/AINative_OKR_CASAN5/.specify/security/incident-runbook.md b/AINative_OKR_CASAN5/.specify/security/incident-runbook.md new file mode 100644 index 0000000..b8f87b6 --- /dev/null +++ b/AINative_OKR_CASAN5/.specify/security/incident-runbook.md @@ -0,0 +1,39 @@ +# CASAN Incident Runbook (C7 / V23) + +When a gate raises an incident (`incident.sh raise `), it is classified, +recorded to `logs/level5/incidents.jsonl`, and for HIGH/CRIT the scoped +kill-switch is engaged automatically + an alert is dispatched. + +## Severity → owner → response + +| Severity | Owner (on-call) | Auto-action | Human step | +|---|---|---|---| +| **CRIT** | security-oncall | kill-switch engaged + alert | Contain now; verify blast radius; do NOT clear until root cause known | +| **HIGH** | ops-oncall | kill-switch engaged + alert | Assess; clear switch only after fix + reviewer sign-off | +| **MED** | tech-lead | recorded + alert | Triage within SLA; batch-fix | +| **LOW** | triage | recorded | Review in retro | + +## Kill-switch operations +```bash +kill-switch.sh status # what is engaged +kill-switch.sh check # gates honor this (exit 2 = stop) +kill-switch.sh clear # turn off (production: reviewer-approved) +``` +Scopes: `project` · `model` · `provider` · `global` (global stops everything). + +## Event → severity +See `incident-severity.map`. Examples: `secret-to-cloud`=CRIT, `tool-write-sensitive`=CRIT, +`dependency-postinstall`=HIGH, `audit-chain-broken`=HIGH, `cost-budget-exceeded`=MED. + +## Postmortem template (fill after resolution) +- **Incident**: +- **Detection**: which gate fired, what signal +- **Blast radius**: scope, what was stopped by the kill-switch +- **Root cause**: +- **Fix**: +- **Prevent recurrence**: new test/gate added (link the fail-able check) +- **Kill-switch cleared by**: at