# Phase 20: Remediation, Approval & Audit Safety - Context

**Gathered:** 2026-07-16
**Status:** Ready for planning

<domain>
## Phase Boundary

Turns a campaign's classification (Phase 19 output) into governed action:
recommended remediation actions are persisted as `proposed`; an operator with
elevated permission can approve specific action(s) with exact params
(recording approver/timestamp); `POST /remediate` proceeds only for approved
actions and is idempotent on re-run; an operator can mark a campaign false
positive (blocked once remediation is already approved/completed); every
state-changing call (classify/approve/remediate/mark-false-positive) writes
an `audit_events` row.

Does NOT cover actually wiring a real external remediation provider (Mimecast
block, MS Graph forwarding-rule toggle, RMM isolate, password reset) — those
are `REMEDEXEC-01..05`, explicitly deferred to v2. Does NOT cover the
Autotask triage note (Phase 21) or the LiveLink approval UI (Phase 22) — this
phase only builds the APIs those consume.

</domain>

<decisions>
## Implementation Decisions

### Remediate Execution Model
- **D-01:** Since no external provider (Mimecast block/purge, MS Graph
  forwarding-rule toggle, RMM isolate, password reset) is wired in this
  milestone — confirmed by direct inspection: `mimecast-client.ts` has no
  block/purge method, `msgraph-client.ts` has no forwarding-rule method, no
  RMM isolate/quarantine method exists anywhere in `lib/services/` —
  `POST /remediate` uses a **simulated internal effect** for ALL 7 action
  types uniformly (no provider distinction yet): it transitions
  `remediation_actions.status` to `completed`, writes the `audit_events` row,
  and returns success. No real external API call is made for any action
  type in this phase. This is the "effect" that REMED-04's idempotency test
  asserts does not duplicate (calling remediate twice on the same approved
  action must produce exactly one `completed` transition / one audit entry,
  not `not_implemented` twice). REMEDEXEC-01..05 (v2) is where a real
  provider call would replace this simulated transition per action type.

### Permission Grants
- **D-02:** `approve` and `remediate` (declared but ungranted in Phase 18
  D-05) are granted to **both `superAdminRole` and `adminRole`** in
  `lib/permissions.ts` — parity with how `analyze` is already granted to
  both. `userRole` still gets only `phishing: ["read"]`. Confirmed via direct
  read of `lib/permissions.ts`: both roles currently have
  `phishing: ["read", "analyze"]` — Phase 20 adds `"approve", "remediate"` to
  both role definitions (the `statement` already has the full vocabulary from
  Phase 18, so this is a role-grant change only, not a new statement key).
  `mark-false-positive` uses the same elevated tier (`approve`-equivalent) —
  see D-04 for why it can't be looser than `approve`.

### Approval Granularity
- **D-03:** `POST /approve` request body specifies WHICH action(s) from the
  classification's `recommended_actions` to approve, and MAY override/narrow
  their params (e.g. `block_sender` scoped to one specific address rather
  than the classifier's broader suggestion). This is NOT a bare
  "approve everything the classifier suggested" call — REMED-02's "recording
  ... the exact approved action parameters" implies operator input, not an
  echo of the classifier's proposal. Each approved action becomes its own
  `remediation_actions` row (`status: 'approved'`, `approved_by`,
  `approved_at`, `params` = the operator-supplied/overridden params) —
  unapproved recommended actions from the classification are simply never
  materialized as rows.

### Mark-False-Positive State Guard
- **D-04:** `POST /mark-false-positive` is **blocked** (explicit
  conflict/error response, not silent success) if the campaign already has
  any `remediation_actions` row with `status IN ('approved', 'completed')`.
  Rationale: prevents a contradictory audit trail where a campaign is both
  "remediated" and "false positive." If no approved/completed remediation
  exists yet, mark-false-positive proceeds freely (this doesn't block
  re-classification or a later un-false-positive correction — that's not in
  scope here; planner's call whether false-positive is reversible).
  Requires the same elevated permission tier as `approve` (D-02) — marking
  false positive is a state-changing security decision, not a read action.

### Claude's Discretion (explicitly deferred to research + planner)
- Exact idempotency detection mechanism for `POST /remediate` re-runs —
  whether it's a status check (`status = 'completed'` short-circuits with
  the same response) or a dedupe key/constraint — as long as calling
  remediate twice on the same approved action produces exactly one
  `completed` transition and one audit entry (D-01's contract).
- Exact `audit_events.actor` value format — likely session email, matching
  the `disabled_by` convention already used for admin actions elsewhere in
  the codebase (per CLAUDE.md's integration-disable section) — planner's
  call, not re-litigated here.
- Whether `POST /approve` allows approving a subset now and a different
  subset later (multiple approve calls against the same classification), or
  is a single one-shot call — planner's call, no strong preference surfaced.
  Must not conflict with D-04's block-after-approved guard.
- Exact JSON request/response shapes for approve/remediate/mark-false-positive
  — follow the project's camelCase API convention; `remediation_actions` and
  `audit_events` are already JSONB-backed tables from migration 097.
- Whether `not_implemented` still has any code path in this phase (e.g. as a
  defensive fallback if a future action type is added without wiring D-01's
  simulated-effect handling) — planner's call; REMED-03's exact wording is
  satisfied by D-01's simulated-effect model since it always "takes or logs
  an action" rather than silently succeeding.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Schema (this phase writes)
- `migrations/097_phishing_triage_schema.sql` — `remediation_actions` table
  (`action_type`, `status` DEFAULT `'proposed'`, `params` JSONB,
  `approved_by`, `approved_at`) and `audit_events` table (`actor`,
  `event_type`, `payload` JSONB) — both currently stubs, this phase is their
  first real writer.

### Action vocabulary (locked upstream, not re-decided)
- `.planning/phases/19-classification-engine/19-CONTEXT.md` D-08 — the 7
  action types (`no_action`, `warn_user`, `disable_forwarding_rule` /
  `block_sender`, `purge_message`, `reset_password`, `isolate_endpoint`) and
  the OR'd `requires_approval` invariant this phase's approve logic must
  respect.
- `lib/services/campaign-classifier.ts` — `classifyCampaign()` output shape
  (`recommendedActions`, `requiresApproval`) that `POST /approve` reads from
  the most recent `classifications` row for the campaign.

### Auth/permissions (this phase grants; Phase 18 declared the vocabulary)
- `lib/permissions.ts` — `phishing` statement already has
  `["read", "analyze", "approve", "remediate"]` (Phase 18 D-05); this phase
  adds `"approve", "remediate"` to `superAdminRole` and `adminRole` (D-02).
- `lib/auth-utils.ts` — `requirePermission(resource, action)` — same
  call-and-early-return pattern as every other `/api/phishing/*` route
  (Phase 18 D-06 convention).
- `app/api/phishing/campaigns/[id]/classify/route.ts` (Phase 19) — the exact
  route-shape precedent (UUID guard + `requirePermission` + delegation) this
  phase's three new routes should copy.

### Provider capability audit (confirms D-01's "no real provider" premise)
- `lib/services/mimecast-client.ts` — no block/purge/quarantine method
  exists (`getHeldMessages`/`releaseHeldMessage` are read/release only, not
  a send-time block).
- `lib/services/msgraph-client.ts` — no mailbox forwarding-rule or password-
  reset method exists.
- No RMM isolate/quarantine method exists anywhere in `lib/services/`.

### Prior phase decisions (for consistency)
- `.planning/phases/19-classification-engine/19-CONTEXT.md` (D-08 action
  vocabulary; D-02 append-only classification history this phase reads from)
- `.planning/phases/18-campaign-grouping-phishing-analysis-api/18-CONTEXT.md`
  (D-05 permission statement vocabulary; D-06 per-route `requirePermission`
  convention)

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `app/api/phishing/campaigns/[id]/classify/route.ts` — exact structural
  precedent (UUID param guard, `requirePermission`, service delegation,
  error shape) for the three new routes in this phase.
- `lib/permissions.ts`'s existing per-role object literals — this phase adds
  two array entries (`"approve"`, `"remediate"`) to two existing role
  objects; no new statement keys needed (Phase 18 D-05 already declared
  them).

### Established Patterns
- Append-only history tables with `campaign_id` FK + `created_at` (same
  shape `classifications` used in Phase 19) — `remediation_actions` and
  `audit_events` should follow the same insert-only shape, no upsert.
- `requirePermission(resource, action)` early-return pattern for every
  `/api/phishing/*` route (Phase 18 D-06) — this phase's three routes follow
  the same shape with `'approve'` / `'remediate'` actions respectively.

### Integration Points
- `POST /approve` reads the campaign's most recent `classifications` row
  (Phase 19) for `recommendedActions`/`requiresApproval`, writes one or more
  `remediation_actions` rows.
- `POST /remediate` reads approved `remediation_actions` rows for the
  campaign, transitions eligible ones to `completed` (D-01), writes
  `audit_events`.
- `POST /mark-false-positive` checks `remediation_actions` state (D-04) before
  proceeding, writes `audit_events`.
- Phase 21 (Autotask triage note) and Phase 22 (LiveLink approval UI) both
  read `remediation_actions`/`audit_events` written by this phase — the
  status values and audit event shape are a direct contract with both.

</code_context>

<specifics>
## Specific Ideas

No specific UI/reference examples surfaced — this is a pure API/backend
phase (`UI hint: no` per ROADMAP.md). Discussion focused entirely on the
approve/remediate/audit contract shape.

</specifics>

<deferred>
## Deferred Ideas

- **Real remediation provider wiring** (Mimecast block, Graph forwarding-rule
  toggle, RMM isolate, password reset) — `REMEDEXEC-01..05`, explicitly v2.
  D-01's simulated-effect model is the placeholder until a v2 phase picks one
  provider at a time.
- **False-positive reversibility** — whether an operator can un-mark a false
  positive later — not discussed, no signal it's needed for this phase;
  planner should treat it as out of scope unless it blocks a success
  criterion.
- **Multi-round approval** (approving additional actions after an initial
  approve call) — noted as Claude's discretion (see `<decisions>`), not a
  locked requirement either way.

</deferred>

---

*Phase: 20-remediation-approval-audit-safety*
*Context gathered: 2026-07-16*
