All lessons › Function C — Application Security with an AI SDLC › The AI SDLC — an Agentic AppSec Pipeline, Before and After Deploy
C2.11

White-box agentic pentest — the source, and what it lets you prove

C2
▶
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/C2.11.mp4
Risk

Full source produces a finding list nobody can act on, because presence is reported where reachability was needed.

Control

Every candidate carries the path that reaches it and the authorisation predicate on that path; unreachable sinks are reported as unreachable rather than dropped.

The lab

Enumerate paths from four entry points in the CyberTravels tree and separate reachable sinks from present ones.

◉Use case relevance

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.

At CyberTravels. 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 this lesson is — Day 0, Day 1, Day 2

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.

Day 0 — why

Full source produces a finding list nobody can act on, because presence is reported where reachability was needed.

Day 1 — how

Enumerate paths from real entry points to sinks, and classify the predicate on each hop as authentication or authorisation.

Day 2 — measure

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 measuredenominator or target
sinks reachable from an entry pointcount — three of five on CyberTravels
reachable sinks carrying only an authentication checkcount

▦The framework, and how it works

   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.

2 · Demo — what the engagement was handed, and what each source proves

3 · Where it breaks — a sink list with no reachability is a scanner dump

4 · The control — paths, predicates, and the authn/authz split

5 · The procedure, as a skill

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:

  • Authentication — who are you?
  • Authorisation — are you allowed to do this?

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.

▶Real time execution as skill

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, Glob

Presence is not reachability, and authentication is not authorisation

White 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.

When to use this

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.

Procedure

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.

Example

  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.

Output contract

{
  "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"]}]
}

Failure modes

  • Counting a session check as authorisation. It is the single most common reason a BOLA survives a white-box engagement.
  • Dropping unreachable sinks. They are the next release's findings.
  • Reporting sinks instead of paths. A sink reachable three ways needs three fixes or one chokepoint, and the list does not say which.
  • Trusting the manifest over the code. The manifest says what an agent may call; the call graph says what it does.

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 C2.11

Then 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:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons C2.11type /c2-11-white-box-agentic-pentest
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons C2.11type /c2-11-white-box-agentic-pentest in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons C2.11type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons C2.11type $c2-11-white-box-agentic-pentest, 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 C2.11

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 C2.11 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.11 --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

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.

✎Your turn

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

Semgrep

Skill source: skills/redteam/whitebox-path-reachability

← PreviousC2.10 · Agentic penetration testing — the loop, and who runs each turn