Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.11.mp4Full source produces a finding list nobody can act on, because presence is reported where reachability was needed.
Every candidate carries the path that reaches it and the authorisation predicate on that path; unreachable sinks are reported as unreachable rather than dropped.
Enumerate paths from four entry points in the CyberTravels tree and separate reachable sinks from present ones.
You were handed the whole repository, and the report is a list of four hundred sinks nobody will read. The two facts that turn it into findings are which sinks a request can actually reach, and which of those check that the caller owns the thing — not merely that they are logged in.
pentest.candidate() joins a finding to C2.5's graph: the path that reaches it, the authorisation predicate on that path, and report_as. Point it at render_template and it comes back "unreachable — reported, not dropped", which is the difference between a white-box report somebody can act on and a list of every sink in the tree.What full source actually buys, which is not a longer list: every candidate carrying the path that reaches it and the authorisation predicate on that path, so presence stops being reported where reachability was the question.
Full source produces a finding list nobody can act on, because presence is reported where reachability was needed.
Enumerate paths from real entry points to sinks, and classify the predicate on each hop as authentication or authorisation.
Sinks reachable from an entry point, and how many of those carry only an authentication check. Three of five, on CyberTravels.
Measure this
| what to measure | denominator or target |
|---|---|
| sinks reachable from an entry point | count — three of five on CyberTravels |
| reachable sinks carrying only an authentication check | count |
a sink list paths, with the predicate on each admin.reset_all present POST /refunds payments.refund present handler [authn: session] db.bookings.read present svc.refund [authz: NONE] ...395 more... -> payments.refund <- FINDING presence is a scanner dump. session proves you are SOMEBODY. reachability + who-is-allowed owner==caller proves you may touch is a finding. THIS object. only the second is authz.
White box is the engagement where you are handed everything: the repository at a pinned commit, the dependency lock, the IaC plan, the tool and MCP manifests, the IAM policy, the API schema. An agent with all of that can enumerate more in an afternoon than a black-box tester finds in a week.
It can also produce the least useful report in security, and usually does: a list of every sink that exists, with no statement about whether anything reaches them or who is allowed to. That list is unactionable in exactly the way a scanner dump is, which wastes the access.
Two distinctions turn the dump into findings, and both are cheap once the agent has drawn the paths rather than the sinks.
Reachability. A sink with no path from any entry point is a different object from one with three. Report the unreachable ones as unreachable — they are one feature away from being findings, and the feature that adds the route will not re-run this analysis.
Authentication is not authorisation. session proves the caller is somebody. owner == caller proves they are entitled to this object. A reachable path carrying only the first kind is the shape of every BOLA finding, and it is the shape of the CyberTravels refund incident — the caller was authenticated from end to end.
The data sources are the point of the mode: name which one establishes each fact, and the finding is checkable rather than asserted.
The skill traces the paths from four CyberTravels entry points to their sinks — a sink being somewhere data can do damage.
On each hop it looks at the check being made and labels it:
Then it separates the sinks you can actually reach from the ones that merely exist in the code. Those are different problems and only one of them is urgent.
The skill — skills/redteam/whitebox-path-reachability/SKILL.md
name: whitebox-path-reachability
description: >-
With source in hand, enumerate paths from real entry points to sinks and name
the authorisation predicate on each, separating reachable sinks from merely
present ones. Use for a white-box engagement, or when a scanner has produced a
finding list nobody can act on.
allowed-tools: Read, Grep, GlobWhite box is the mode where you are handed everything, and the failure it produces is specific: a long list of sinks that exist, with no statement about whether anything can reach them or who is allowed to. That list is unactionable in exactly the way a scanner's output is, which is a waste of the access you were given.
Two distinctions do the work, and both are cheap once the paths are drawn.
A white-box engagement, a threat model that needs to be argued rather than asserted, and any time a finding list is long and nobody is fixing it.
1 — Write down what you were handed, and what each source establishes. Repository at a pinned commit, dependency lock, IaC plan, tool and MCP manifests, IAM policy, API schema, prior findings. Naming the source per fact is the difference between a finding and an assertion.
2 — Enumerate paths, not sinks. Entry point, every hop, the sink. A sink with no path from any entry point is a different kind of object from one with three.
3 — Record the predicate on each hop, and classify it. session and service_account prove the caller is somebody. owner==caller and role checks prove they are entitled to this object. A path carrying only the first kind is the shape every BOLA finding has.
4 — Report the unreachable sinks as unreachable, not as nothing. They are one route away from being findings, and the feature that adds the route will not re-run this analysis on its own.
AUTHN ONLY POST /refunds handler -> svc.refund -> payments.refund
authn: session authz: NONE ON ANY HOP
sinks present in the tree 7
sinks reachable from an entry 5
of those, authenticated only 3
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.
{
"data_sources": [{"source": "str", "establishes": "str"}],
"paths": [{"entry": "str", "hops": [["str", "str|null"]], "sink": "str",
"reachable": true, "authn": ["str"], "authz": ["str"]}],
"counts": {"present": 0, "reachable": 0, "authn_only": 0, "unreachable": 0},
"findings": [{"sink": "str", "entry": "str", "has": ["str"]}]
}
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.
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 C2.11Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-11-white-box-agentic-pentest:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.11 | type /c2-11-white-box-agentic-pentest |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.11 | type /c2-11-white-box-agentic-pentest in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.11 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.11 | type $c2-11-white-box-agentic-pentest, or run /skills |
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 C2.11Neither 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 C2.11 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.11 --diffOn 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.
Of seven sinks in the tree, five are reachable from an entry point and two are reported as present-but-unreachable rather than dropped. Three of the reachable five carry only a session or service-account check — authentication, not authorisation — including the refund path, which is the incident's shape stated before the incident.
Take one endpoint in your own estate and trace it to its sink by hand. Count the hops that check the caller is somebody against the hops that check they own this object. If the second count is zero, you have found your first finding.
Tools used
Skill source: skills/redteam/whitebox-path-reachability