All lessons › Function E — The Agentic SOC › Understand — Correlation, Intel and the Hunt
E3.7

Scoping an agentic incident — following the delegation graph

E3
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/E3.7.mp4
Risk

The initiating agent is not the acting one.

Control

Reconstruct the action chain across all three planes.

The lab

Scope a multi-agent incident end to end.

◉Use case relevance

The agent acted for eleven minutes on delegated credentials at machine speed. Scoping that means reconstructing blast radius from identity and egress logs, because asking what it touched is not a question anyone can answer from memory.

At CyberTravels. delegation_graph() builds the edges out of the act chains B2.3 made nest, and blast_scope() walks forward from one principal. Scoping to the acting agent alone returns the last hop; the run that asked for it is one edge back.

◴What this lesson is — Day 0, Day 1, Day 2

Scoping an agentic incident by walking the delegation graph, because the agent that made the call is not the one that started the work, and the difference is one edge back.

Day 0 — why

The agent that touched the resource is usually the last actor in a chain, and the earlier ones had more authority, not less.

Day 1 — how

Scope along the delegation graph rather than the host list.

Day 2 — measure

Resources found by walking the graph versus by scoping the acting agent alone. The undercount grows with delegation depth.

Measure this

what to measuredenominator or target
resources found by walking the delegation graph, against scoping the acting agent alonetwo counts; the undercount grows with delegation depth

▦The framework, and how it works

   11 minutes at machine speed

   identity log ---+                    +--> resources touched
                   +--> reconstruct --> +--> data read
   egress log   ---+                    +--> destinations reached
                                        +--> credentials used

   the question "what did it touch" is not answerable from memory

Scoping answers "what was touched?" For a host-based incident you enumerate hosts. For an agentic incident, the host list is not the boundary — the chain that delegated the work is.

The agent that touched the resource is usually the last actor in a chain. If you scope only that actor, you miss everything the earlier actors reached — and because authority narrows down the chain, the earlier actors typically had more access, not less.

The undercount is systematic and it grows with delegation depth, which is the operational reason C2.0 bounds delegation depth in the first place.

Anchor → E1.0. Scope decides what understood covers. Following the last actor rather than the delegation graph ends the interval early and wrong, and because authority narrows down the chain the earlier actors had more access — so the undercount grows with depth.

2 · Demo — scope the chain, not the actor

3 · Where it breaks — the undercount grows with depth

4 · The control — scope from the act chain, then widen by shared resources

5 · Scoping as a skill

Scoping a human incident asks one question: where did somebody log in?

Scoping this one asks a different question: what did the agent decide? Every single action it took was properly authorised, so nothing looks wrong at the login layer. There is nothing there to find.

Two fields in the output carry most of the weight:

  • reach and confirmed_exfiltration are separate numbers. Reach is the scope until you prove otherwise. The smaller number must never stand in for the larger one when deciding whether to notify people.
  • does_not_stop makes containment state its own limits out loud, rather than implying it stopped everything.

▶Real time execution as skill

The skill — skills/secops/incident-scoping/SKILL.md

name: incident-scoping
description: >-
  Scope an incident in which an agent was the actor — what it touched, what it
  changed, what it may have exfiltrated, and where containment must cut. Use
  when responding to an agent-involved incident, reconstructing what an
  autonomous system did, deciding what to revoke, or sizing notification
  obligations.
allowed-tools: Read, Grep, Bash

Scoping an agentic incident

Scoping a human incident asks where someone logged in. Scoping an agentic one asks what the agent decided, because a compromised agent's actions are all individually authorised. Nothing looks anomalous at the authentication layer; the anomaly is in the sequence.

When to use this

Any incident where an agent, an automated pipeline, or an AI-driven tool performed actions under investigation.

Procedure

1 — Fix the window. Establish the first suspicious decision, not the first alert. Work backwards from the earliest action you cannot explain; the trigger is usually earlier than the detection by the length of one task loop.

2 — Reconstruct the decision chain. For the window, list every action with the input that motivated it. The critical question is which input entered the context from outside the trust boundary — a fetched page, an issue comment, a dependency's README, a tool description. That input is the likely root cause, and it is invisible if you only log tool calls and not their justification.

3 — Separate authority from behaviour. For each action ask: was it within the agent's granted authority? Actions that were authorised but wrong tell you the grant was too broad. Actions that exceeded authority tell you a control failed. These lead to different fixes and must not be pooled.

4 — Establish data reach. What did the agent read, and where could it have sent it? Reach is bounded by the agent's egress, not by what it appears to have sent — a request body you cannot see is still reach. State reach and confirmed exfiltration as separate numbers, and never let the smaller one stand in for the larger in a notification decision.

5 — Decide the containment cut. Options, in increasing cost: revoke the agent's credential, disable the trigger, quarantine the workload, disable the whole class of agents. Choose by blast radius, not by convenience, and record what the cut does not stop — sibling agents on the same shared service account almost always survive a credential revocation aimed at one of them.

6 — Preserve evidence the agent could alter. If the agent can write to the log store, the logs are not evidence. Snapshot first, then contain.

Example

Input — the fixture committed at the top of scripts/incident_scoping.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

chain                     dana@corp → orchestrator → patch-agent → deploy-agent
scoped_last_actor_only    ['cluster-prod']
scoped_whole_chain        ['cluster-prod', 'queue-tasks', 'repo-core', 'repo-infra', 'repo-payments', 'vault-dev']
missed_by_naive_scoping   ['queue-tasks', 'repo-core', 'repo-infra', 'repo-payments', 'vault-dev']
undercount_factor         6.0

Scoping the last actor finds one cluster. The chain reached six
resources, including a payments repository and a dev vault.

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "window": {"first_suspicious_action": "str", "detected_at": "str", "gap_seconds": 0},
  "chain": [{"action": "str", "motivating_input": "str",
             "input_origin": "operator|internal|external_untrusted",
             "within_authority": true}],
  "root_cause": {"input": "str", "origin": "str", "why_trusted": "str"},
  "authority": {"authorised_but_wrong": 0, "exceeded_authority": 0},
  "data": {"reach": ["str"], "confirmed_exfiltration": ["str"], "egress_bounded_by": "str"},
  "containment": {"cut": "credential|trigger|workload|class",
                  "does_not_stop": ["str"], "evidence_snapshotted_first": true},
  "clock": {"regulatory_trigger": false, "basis": "str"}
}

Failure modes

  • Scoping by authentication. Every action was authenticated; that is the point.
  • Logging tool calls without their motivating input. Root cause then cannot be established at all.
  • Reporting confirmed exfiltration as the scope. Reach is the scope until proven otherwise.
  • Revoking one agent's token when the identity is shared, and calling it contained.
  • Containing before snapshotting a log store the agent can write to.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons E3.7

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick e3-7-scoping-an-agentic-incident:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons E3.7type /e3-7-scoping-an-agentic-incident
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons E3.7type /e3-7-scoping-an-agentic-incident in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons E3.7type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons E3.7type $e3-7-scoping-an-agentic-incident, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py E3.7

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at E3.7 --out work/cybertravels
python3 scripts/checkpoint.py --at E3.7 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

Scoping the last actor finds cluster-prod alone; the whole chain reaches six resources, missing five, with an undercount factor of 6.0. The undercount grows with each hop. Transitive scoping adds five second-order identities that shared a resource, explicitly marked as in scope rather than confirmed compromised.

✎Your turn

For your last incident involving a service account, recompute the scope by walking what else that account could reach. The number is almost always larger than what was written in the report.

Tools used

OpenTelemetryKimi K2Claude Haiku 4.5

Skill source: skills/secops/incident-scoping

← PreviousE3.6 · Plan, then replan — an investigation that changes its mind