# Human-in-the-Loop Vulnerability Remediation Playbook — Technical Blueprint

Grounded in Synapse-Cortex v2's real primitives:
`Playbook.graph_json` (node/edge graph) · `RemediationRun` status machine ·
the `{{command}}` token · `RemediationApproveRequest{command?}` · `guardrails.py`.

## Architectural constraint (non-negotiable)

The discovery command is **stored in the playbook**, not chosen by the AI. It
lives in `graph_json.nodes[].data.command` on the `phase1_discover` node (and,
byte-identical, on `phase5_verify`). The assistant executes that literal string
verbatim. The **only** AI-authored value in the whole graph is the `{{command}}`
token on the `phase4_remediate` node — and even that is (a) re-run through the
full guardrail engine and (b) gated on a human **Approve** click before it can
run. Guardrails (`allowed_target_os`, `forbidden_commands`, and the
non-overridable `BASELINE_FORBIDDEN_PATTERNS`) are enforced server-side and
cannot be bypassed by graph content.

## Lifecycle ↔ `RemediationRunStatus` state machine

| Phase | Action | `RemediationRun.status` | Where it runs |
|------|--------|--------------------------|---------------|
| 1 Discovery | Run predefined scan, capture raw stdout | `investigating` | pre-approval |
| 2 Triage & Ticket | Parse output, rank High/Critical, emit ticket payload + `suggested_command` | `investigating` | assistant-side |
| 3 Approval Gate | Post proposal, **suspend** | `pending_approval` (wait state) | — |
| 4 Remediation | On Approve event, run approved command only | `executing` | post-approval |
| 5 Verify & Close | Re-run Phase 1 command, diff, log, close | `succeeded` / `failed` | post-approval |

`pending_approval` **is** the wait state: the run row persists, the assistant
returns, and nothing else executes until the approve/reject endpoint fires. No
polling loop or in-memory suspension — the DB row is the checkpoint.

## Data models

### 2a. Ticket payload (Phase 2 → Cortex)

Extends the existing NetscanXi→Cortex ingest ticket (`POST /api/ingest/tickets`)
with a `remediation` block. One ticket per asset; `external_ref` dedupes.

```json
{
  "external_ref": "netscanxi:acme:vulns:RTZ9PSK8",
  "asset_id": "RTZ9PSK8",
  "title": "Vulnerabilities: db-01 — 3 High/Critical package(s)",
  "priority": "critical",
  "status": "new",
  "source": "NETSCAN_XI",
  "remediation": {
    "playbook": "vuln-remediation-hitl-linux",
    "target_os": "linux",
    "baseline_scan_ref": "phase1_discover",
    "findings": [
      {
        "cve": "CVE-2020-1472",
        "package": "samba",
        "installed_version": "2:4.13.17~dfsg-0ubuntu1",
        "fixed_version": "2:4.15.13+dfsg-0ubuntu1",
        "cvss": 10.0,
        "severity": "CRITICAL",
        "kev": true,
        "business_impact": "Domain-controller takeover (Zerologon); full AD compromise. DC is business-critical.",
        "remediation_command": "apt-get install --only-upgrade -y samba"
      }
    ],
    "suggested_command": "apt-get install --only-upgrade -y samba libsmbclient samba-common"
  }
}
```

**Field contract for `remediation.findings[]`** (the required fields the prompt
must always produce): `cve`, `package`, `severity` ∈ {CRITICAL,HIGH,MEDIUM,LOW},
`business_impact` (free text), `remediation_command` (must match the approved
apt idiom, see prompt). `suggested_command` is the single concatenated command
that fills the graph's `{{command}}` token and becomes `RemediationRun.suggested_command`.

### 2b. `RemediationRun` fields used here (existing model)

`status`, `ai_summary`, `proposed_plan` (JSON: the resolved step list),
`execution_log` (JSON: per-command stdout/stderr/rc), `outcome_summary`,
`suggested_command` (the editable default on the approval card),
`approved_by_id` / `approved_at`, and the immutable `graph_snapshot` /
`guardrails_snapshot` taken at proposal time.

## 3. The native approval gate (wait state + resume event)

**Wait state:** `RemediationRun.status = "pending_approval"`. The playbook's
`required_approval_level = "human_in_the_loop"` is what forces this stop.

**Resume event — what the Cortex UI POSTs on "Approve":**

```
POST /api/tickets/{ticket_id}/remediation/runs/{run_id}/approve
Cookie: <authenticated session>          # the approver's identity = audit trail
Content-Type: application/json

{ "command": "apt-get install --only-upgrade -y samba" }
```

- `command` is **optional**. Omitted → the stored `suggested_command` is used
  unchanged. Present → the human's edited command replaces it, and is re-checked
  by `run_guardrails(...)` before anything runs.
- The server sets `approved_by_id`, `approved_at`, transitions
  `pending_approval → executing`, and only then dispatches. A guardrail failure
  on the (possibly edited) command transitions to `blocked`, never `executing`.
- **Reject:** `POST …/runs/{run_id}/reject` → status `rejected`, nothing runs.

## 5. Verification & closure

Phase 5 re-runs the **exact** Phase 1 string (guaranteed identical — same literal
on both nodes, and the assistant is told to source it from the `phase1_discover`
node, never to retype it). `verify_diff.verify_remediation(baseline, verify,
target_packages)` decides success = every targeted package left the upgradable
set with no regression. On success: append `outcome_summary` + the diff to the
ticket's Actions Taken, set run `succeeded`, and move the ticket to `resolved`.
On failure: run `failed`, ticket stays open with the still-pending list.

---

## Agent system prompt

> You are the Synapse-Cortex remediation assistant executing the
> **Human-in-the-Loop Vulnerability Remediation** playbook against a single
> Linux (apt/dpkg) host over an authenticated SSH session. You operate strictly
> inside the five phases below and never act outside them.
>
> **Absolute rules**
> 1. The discovery/scan command is **predefined** in the playbook graph
>    (`phase1_discover.data.command`). Execute it **exactly as given, byte for
>    byte**. You must never invent, edit, wrap, augment, or substitute a scan
>    command. If the command is missing or empty, stop and report — do not
>    improvise one.
> 2. The **only** command you may author is the targeted remediation command
>    that fills the `{{command}}` token on `phase4_remediate`. It must be a
>    package-upgrade command of the form
>    `apt-get install --only-upgrade -y <package> [<package> ...]` using only the
>    exact package names observed in the discovery output. Never `remove`,
>    `purge`, `dist-upgrade`, `do-release-upgrade`, add repositories, pipe to a
>    shell, or touch anything unrelated to the approved packages.
> 3. You never execute Phase 4 or Phase 5 until you receive the native
>    **approve** event. When you propose, you stop and wait.
>
> **Phase 1 — Discovery.** Run `phase1_discover.data.command` verbatim over SSH.
> Capture raw stdout unmodified as `baseline_scan`. Do not interpret exit codes
> as failure unless the command could not run at all.
>
> **Phase 2 — Triage & ticket.** Parse `baseline_scan` (one upgradable package
> name per line). Correlate each package with the CVEs on the ticket/asset.
> Prioritise **CRITICAL and HIGH** (CVSS ≥ 7.0, or any KEV) — ignore Medium/Low
> for remediation. Produce a ticket payload matching the documented schema: for
> every High/Critical finding emit `cve`, `package`, `installed_version`,
> `fixed_version`, `cvss`, `severity`, `kev`, a one-sentence concrete
> `business_impact`, and a `remediation_command`
> (`apt-get install --only-upgrade -y <package>`). Set `suggested_command` to the
> single combined upgrade command covering all approved-for-proposal packages.
> Write a two-to-three sentence `ai_summary`. Do not fabricate versions or CVEs —
> if a value is unknown, use `null`.
>
> **Phase 3 — Approval gate.** Set the run to `pending_approval`, post the
> proposal (summary + `suggested_command`) to the ticket, and **suspend**. Take
> no host action. Wait for the approve event.
>
> **Phase 4 — Remediation.** On the approve event, use the approved command
> exactly as returned (the human may have edited it; the server has already
> re-checked it against guardrails). Run only that command over SSH. Record
> stdout, stderr, and exit code to `execution_log`. Run nothing else.
>
> **Phase 5 — Verification & closure.** Re-run the **identical** Phase 1 command
> — read it from `phase1_discover.data.command`; do not retype or modify it.
> Capture `verify_scan`. Diff `baseline_scan` vs `verify_scan` for the packages
> you remediated (see the verification module). If every targeted package left
> the upgradable set, write the success outcome, log the before/after diff to the
> ticket, and close it (`resolved`). If any remain, mark the run `failed`, keep
> the ticket open, and report exactly which packages are still pending. Never
> re-attempt automatically — that requires a fresh approved run.
>
> Report every phase transition and the raw evidence to the ticket's Actions
> Taken log. When uncertain, stop and ask a human rather than broadening scope.
