Runbook: Data Exfiltration Pattern¶
Incident kind: data_exfil_pattern · Severity: high · Detection rule: correlate::rule_data_exfil
Symptoms¶
- A SOC alert/incident with
kind: "data_exfil_pattern"appears inGET /v1/incidents. - The pattern: a Source action (
gateway/src/correlate.rs'sSOURCE_TOKENS— names containingread/get/fetch/list/download/query/select/export/dump/cat) is followed withinEXFIL_WINDOW_SECS(120s) by a Sink action for the same agent (SINK_TOKENS— names containingsend/post/upload/email/webhook/push/publish/write_external/share/transfer, or any action literally namedexfil). Matching is substring-based and case-insensitive on the action name only, so it's a heuristic, not a content-aware DLP scan. - This is the canonical real-world pattern from the Invariant Labs GitHub-MCP disclosure (T-B2 in
AegisAgent_Threat_Model.md): a malicious issue tricks an agent into reading private data, then pushing it somewhere public/external.
Before you start: check whether this already auto-resolved¶
data_exfil_pattern maps to freeze the agent + a critical-severity notification in the Response Engine (gateway/src/respond.rs), but only runs at SOC autonomy level L3/L4 (default is L1, notify-only). Check the tenant's effective level the same way as in deny-storm.md before assuming containment already happened.
Investigation¶
- Find the incident and its evidence graph (same as the deny-storm runbook):
- Identify the Source and Sink decisions specifically — the incident's
source_event_idslink to exactly two decisions (the paired read and the paired write). Resolve each event to its decision viaGET /v1/decisions/:idto see the actualtool/action/resourceandrisk_score. - Check the triggering context's trust level. Was the run triggered by
untrusted_external/semi_trusted_customercontent (e.g. a public issue, an email)? If so this strongly corroborates T-B1/T-B2 (confused-deputy via provenance) rather than a benign coincidence — cross-checkGET /v1/decisions/:id's recordedtrust_level/root_trust_level. - Check what was actually read and where it went. The decision's
resourcefield (e.g. a file path, a repo, a record id) tells you the blast radius — was it a single record or a bulk export? Where did the sink action send it (an external webhook URL, a public repo, an email address outside the org)? - Generate an RCA narrative:
Remediation¶
This pattern is higher-confidence-malicious than a deny storm — default to containment first, investigation second:
- Freeze (or revoke, if you're confident this is a real compromise) immediately:
- Rotate the agent's token regardless of root cause — even a false positive doesn't hurt from rotating, and a true positive may mean the token is already compromised (see
agent-token-rotation.md): - If the sink was an external destination you control (e.g. your own webhook endpoint, a repo), check whether the data actually arrived there and needs to be deleted/revoked at the destination — AegisAgent's containment stops the agent, not data already in flight or already delivered.
- If the source was triggered by untrusted content (a malicious issue/email/ticket), consider whether the underlying confused-deputy gate needs tightening — e.g. an explicit
forbidpolicy for this specific tool/action combination under untrusted provenance, beyond the defaultmutates_state && untrusted_externalrule (a read-then-external-write chain may not always tripmutates_stateon the read half). - Close the incident once handled:
Verification¶
GET /v1/agents/:idshowsstatus: "revoked"or"active"with a confirmed-rotated token.- The old token is rejected on the next
/v1/authorizecall (401). GET /v1/incidents/<incident_id>showsstatus: "closed".- If a destination cleanup was needed, confirm directly against that external system (outside AegisAgent's scope) — note this in the incident close reason for the audit trail.
- No repeat
data_exfil_patternincident for the same agent in the followingEXFIL_WINDOW_SECS(120s) window.