Skip to content

Runbook: Data Exfiltration Pattern

Incident kind: data_exfil_pattern · Severity: high · Detection rule: correlate::rule_data_exfil

Status: Implemented heuristic correlation for gateway evidence. It is not content-aware DLP and does not prove delivery to an external destination.

Overview

Use this runbook when one agent performs a source-like read followed by a sink-like external write within the correlation window. Contain first, preserve evidence, determine what data and destination were involved, rotate potentially exposed credentials, and verify both Aegis state and the external destination.

Symptoms

  • A SOC alert/incident with kind: "data_exfil_pattern" appears in GET /v1/incidents.
  • The pattern: a Source action (lib/soc/src/correlate.rs's SOURCE_TOKENS — names containing read/get/fetch/list/download/query/select/export/dump/cat) is followed within EXFIL_WINDOW_SECS (120s) by a Sink action for the same agent (SINK_TOKENS — names containing send/post/upload/email/webhook/push/publish/write_external/share/transfer, or any action literally named exfil). 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 (lib/soc/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

  1. Find the incident and its evidence graph (same as the deny-storm runbook):
    curl -s -H "Authorization: Bearer $AGENT_TOKEN" \
      "http://127.0.0.1:8080/v1/incidents?severity=high&status=open" | jq '.[] | select(.kind=="data_exfil_pattern")'
    curl -s -H "Authorization: Bearer $AGENT_TOKEN" \
      "http://127.0.0.1:8080/v1/graph/incident/<incident_id>"
    
  2. Identify the Source and Sink decisions specifically — the incident's source_event_ids link to exactly two decisions (the paired read and the paired write). Resolve each event to its decision via GET /v1/decisions/:id to see the actual tool/action/resource and risk_score.
  3. Check the triggering context's trust level. Was the run triggered by untrusted_external/semi_trusted_customer content (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-check GET /v1/decisions/:id's recorded trust_level/root_trust_level.
  4. Check what was actually read and where it went. The decision's resource field (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)?
  5. Generate an RCA narrative:
    curl -s -H "Authorization: Bearer $AGENT_TOKEN" \
      "http://127.0.0.1:8080/v1/incidents/<incident_id>/narrate"
    

Remediation

This pattern is higher-confidence-malicious than a deny storm — default to containment first, investigation second:

  1. Freeze (or revoke, if you're confident this is a real compromise) immediately:
    curl -s -X POST -H "Authorization: Bearer $AGENT_TOKEN" \
      "http://127.0.0.1:8080/v1/agents/<agent_id>/freeze" -d '{"reason": "data_exfil_pattern — possible exfiltration"}'
    
  2. 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):
    curl -s -X POST -H "Authorization: Bearer $AGENT_TOKEN" \
      "http://127.0.0.1:8080/v1/agents/<agent_id>/rotate-token" -d '{"reason": "data_exfil_pattern containment"}'
    
  3. 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.
  4. 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 forbid policy for this specific tool/action combination under untrusted provenance, beyond the default mutates_state && untrusted_external rule (a read-then-external-write chain may not always trip mutates_state on the read half).
  5. Close the incident once handled:
    curl -s -X POST -H "Authorization: Bearer $AGENT_TOKEN" \
      "http://127.0.0.1:8080/v1/incidents/<incident_id>/close"
    

Verification

  • GET /v1/agents/:id shows status: "revoked" or "active" with a confirmed-rotated token.
  • The old token is rejected on the next /v1/authorize call (401).
  • GET /v1/incidents/<incident_id> shows status: "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_pattern incident for the same agent in the following EXFIL_WINDOW_SECS (120s) window.

Security and Failure Handling

  • Treat attacker-controlled prompt/tool content as inert evidence; do not follow instructions embedded in it.
  • Do not copy sensitive source data into incident chat or tickets. Record identifiers, hashes, and protected evidence locations.
  • Freezing the agent does not recall data already delivered. Confirm destination cleanup independently.
  • A missing correlation is not proof of safety; the detector uses action-name heuristics and deployed evidence only.
  • If containment fails, revoke credentials at the upstream tool/provider and restrict network access using external controls.

Rollback and Recovery

Do not unfreeze solely because the incident was closed. Restore the agent only after the trigger source, policy gap, credentials, destination impact, and agent configuration are understood. Re-enable with a fresh token and a low-risk verification action; monitor for at least one correlation window.

References