Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.8.mp4A SAST finding is a hypothesis, and hypotheses get argued about instead of fixed.
Stage 12: generate and run an actual exploit against the sandbox, so the finding is confirmed or dropped.
Turn static findings into executable probes against the replica and separate confirmed from unconfirmed.
An investigation lands — somebody else's, into somebody else's incident. The research function's job is not to summarise it. It is to index it so each finding can be cited alone, type every control, and give each one an owner who will test it.
exploit_sql_injection turns one row into four across three owners; exploit_idor asks for booking 2 as Dana and gets Priya's, with no payload and nothing malformed — which is why stage 7 could not see it. An exploit that does not fire returns undetermined, never refuted.Turning hypotheses into demonstrations against the replica, and the half that matters as much: a hypothesis that cannot be demonstrated is dropped rather than shipped as a medium somebody has to carry. An exploit that does not fire is undetermined, not refuted.
A static finding is a hypothesis, and hypotheses get argued about in triage meetings instead of fixed.
Generate and run an actual exploit against the sandbox, so each finding is confirmed or dropped.
Findings confirmed by execution versus refuted. Both are useful answers; only one of them is a ticket.
Measure this
| what to measure | denominator or target |
|---|---|
| findings confirmed by execution, against those refuted | two counts; only one of them is a ticket |
report ------> register ------> owners
T<row>.<n> threat typed P / D / C
E<row>.<n> evidence anchored to a control family
C<row>.<n> control assigned to a lesson that tests it
10 rows . 34 threats . 40 evidence items . 40 controls
rows 1, 2 and 5 share one surface:
[ artifact repository ] = covert channel
= supply-chain target
= escape vector
filed apart, three teams each fix a third and the surface remainsStage 12 — Dynamic exploitation. The stage that converts an argument into a fact.
Everything Phase 3 produced is a hypothesis: the code looks vulnerable and the sink appears reachable. Hypotheses get argued about in triage meetings. An executed exploit does not — either the probe achieved the effect or it did not.
Two things this stage produces that static analysis cannot:
The discipline that makes it trustworthy is that the probe must assert a concrete effect — rows returned that should not be, a file read outside the root — not merely that the request did not error. "No exception" is the DAST equivalent of a shape check, and C2.0 already established what those are worth.
Where you are in the pipeline. `` [Ingestion & Mapping] ──> [Threat Modelling] ──> [Discovery] └─ stages 1-4 └─ stages 5-6 └─ stages 7-10 ──> [Dynamic Validation] ──> [Reporting] └─ stages 11-14 └─ stage 15 ``The most common DAST bug: treating "the request succeeded" as confirmation. Both probes below hit the app and return 200-equivalents; only one of them proves anything.
A dynamic probe proves nothing unless its assertion can fail. The skill runs the probes against a live build with a control probe alongside, then re-runs them under a weak assertion so you can watch it flag the control too.
The skill — skills/appsec/dynamic-exploitation-probe/SKILL.md
name: dynamic-exploitation-probe
description: >-
Run probes against a live build to turn a static finding into a demonstrated
one, with a control probe and an assertion strong enough to tell them apart.
Use when confirming exploitability, or when a dynamic test reports everything
as vulnerable.
allowed-tools: Read, Grep, Glob, BashA dynamic probe proves nothing unless its assertion can fail. A weak assertion — "the response contains rows", "the request did not error" — confirms the control probe as readily as the attack, and a test that confirms everything has measured nothing.
After static analysis has produced hypotheses, in a replica confirmed by an isolation check, and before any finding is reported as exploitable.
1 — State each hypothesis as a prediction. Not "SQL injection in list_reports" but "requesting one owner returns rows for more than one". A hypothesis you cannot write as a prediction is not ready to test.
2 — Include a control probe. A well-formed, benign request whose result is known. It is the thing that tells you your assertion discriminates.
3 — Write the assertion against the prediction. Count owners returned, not whether rows came back. Read the file's contents, not whether a path resolved. The difference between these is the entire value of the stage.
4 — Run all probes, including the control, and record raw results. Then apply the assertion. Keep both: the raw result is what somebody re-reads when they doubt the finding.
5 — Demonstrate the weak assertion too. Run the same probes under the loose check and show it flagging the control. That comparison is what stops the next person writing one.
6 — Mark each hypothesis confirmed, refuted, or untested — and never "probably". A hypothesis whose probe could not run is untested, which is different from refuted and much more common.
Input — the fixture committed at the top of scripts/dynamic_exploitation_probe.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:
normal use:
list_reports('test-user-a') → [(1, 'test-user-a', 100), (2, 'test-user-a', 250)]
fetch_doc('readme.txt') → public readme
probe cwe exploited observation
--------------------------------------------------------------------------
P-01 CWE-89 True 4 rows, owners=['test-user-a', 'test-user-b', 'test-user-c']
P-02 CWE-22 True returned 'synthetic-not-real-9999'
P-03 CWE-89 False 2 rows, owners=['test-user-a']
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.
{
"hypotheses": [{"id": "str", "prediction": "str"}],
"probes": [{"id": "str", "kind": "attack|control", "raw": "str"}],
"assertions": [{"id": "str", "strong": true, "verdict": "confirmed|refuted|untested"}],
"weak_assertion_run": {"flagged": ["str"], "includes_control": true}
}
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.8Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-8-dynamic-exploitation-dast:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.8 | type /c2-8-dynamic-exploitation-dast |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.8 | type /c2-8-dynamic-exploitation-dast in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.8 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.8 | type $c2-8-dynamic-exploitation-dast, 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.8Neither 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.8 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.8 --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.
The SQL injection probe returns rows for three owners when one was requested, and the traversal probe returns the synthetic token from outside the document root; the control probe returns a single owner and is not flagged. The weak assertion confirms all three including the control. Stage 12 marks two findings CONFIRMED and one UNVALIDATED for having no probe.
Look at your DAST assertions. If any of them checks only for a non-error response, it is confirming findings it has not tested — and the control probe above is how you prove that in five minutes.
Tools used
Skill source: skills/appsec/dynamic-exploitation-probe