Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.15.mp4Severity is a label copied from the rule, so the queue is ordered by something that predicts nothing.
Stage 15: calibrate severity from sandbox evidence, then report per-stage economics rather than a finding count.
Recalculate severity from confirmed exploitation and reachability, then produce the per-stage escape economics.
CVSS scores the vulnerability. Your engineers are asking about this system, with this data, behind this control, and the number that answers them is not the one on the badge.
calibrate() scores from what this run actually established: demonstrated, on the money path, mutates state, reachable. Same CWE and same rule severity, and issue_refund comes back critical while dead render_template comes back info — a queue ordered by the rule's severity puts them in the same place. economics() is what you report instead of a finding count.Calibrating severity from what this run established rather than copying it from the rule that fired, and reporting per-stage economics instead of a finding count — what each stage cost and what it removed.
Severity copied from the rule orders the queue by something that predicts nothing.
Calibrate severity from sandbox evidence, and report per-stage economics rather than a single total.
Cost per confirmed finding at each stage, which is what tells you which stage to invest in next.
Measure this
| what to measure | denominator or target |
|---|---|
| cost per confirmed finding, per stage | currency per finding; it is what says which stage to invest in next |
CVSS 9.8 your system
+----------------+ +---------------------------+
| network | | internal only |
| no auth | vs | behind SSO |
| full impact | | read-only replica |
+----------------+ +---------------------------+
| |
the badge the number engineers act on
confirmed-by-exploitation beats bothStage 15 — Severity calibration and reporting. The pipeline's output, and the stage where its credibility is won or lost.
Most severity is a label copied from the rule that fired: this is a CWE-89, so it is high. That number predicts nothing, because it ignores everything the pipeline has just learned:
Calibrated severity uses all four. A confirmed, reachable finding that chains into account takeover is not the same as an unvalidated finding in dead code, even when both are CWE-89.
The second half of this stage is the report, and the useful report is not a finding count. It is per-stage economics: where bugs are caught, where they escape, and what each escape costs — because that is what decides next quarter's budget.
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 reporting skill carries one rule that decides most of the credibility of a security report: a finding that did not reproduce may not be Critical. Cap it at Medium and say so in the same sentence, so the reader never has to cross-reference an appendix to learn that the headline finding is theoretical.
The contract enforces the habit by requiring severity_inputs next to every severity. One overclaimed Critical costs more trust than ten honest Lows.
Now report the same findings without the cap.
The skill — skills/appsec/appsec-triage-report/SKILL.md
name: appsec-triage-report
description: >-
Calibrate severity and write the security report a team will actually act on.
Use when asked to write up findings, assign or justify severity, produce a
pentest or code-review report, summarise a scan for engineers or leadership,
or decide what to escalate.
allowed-tools: Read, WriteCovers stage 15. Every earlier stage is measured here: a defect that was found, deduplicated, verified and reproduced still counts for nothing if the report gets it fixed slowly or not at all.
At the end of a review, or whenever findings must be handed to someone who did not do the analysis.
findings (Phase 3), validations (Phase 4), and plan.deferred (Phase 2). The last one is not optional — it is the scope statement.
Calibrate severity, and show the calibration. Severity is a function of impact and reachability, and both are already computed upstream. State them:
| Severity | Requires |
|---|---|
| Critical | reproduced, unauthenticated path, sink is subprocess/deserialisation/credential |
| High | reproduced, or confirmed with an authenticated path to a dangerous sink |
| Medium | confirmed, feasible, but mitigated in depth or requires elevated access |
| Low | confirmed, not feasible in this deployment |
| Informational | needs_human, or hardening with no demonstrated path |
A finding that did not reproduce may not be Critical. If Phase 4 set reproduced: false, cap it at Medium and say why in the same sentence — the reader must not have to cross-reference an appendix to learn that the headline finding is theoretical.
Separate demonstrated from asserted. Two sections, always. The credibility of the whole report comes from the reader being able to tell them apart without effort, and one overclaimed Critical costs more trust than ten honest Lows.
Write for the person who will fix it. Each finding needs: where it is, what an attacker does with it, the observable that proves it, the fix, and what the fix costs. Lead with the fix — the reader is deciding what to do, not learning the CWE taxonomy.
State the scope honestly. What was analysed, what was deferred and why, what the tooling cannot see. A report that hides its gaps invites the reader to treat silence as coverage.
Report accuracy, never conformance. "100% schema-valid" is a statement about the serialiser and is true of an empty result. If a false-positive rate is known — Phase 4 measures one every run — report that instead.
Input — the fixture committed at the top of scripts/appsec_triage_report.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:
id rule sev calibrated score
----------------------------------------
F-01 high critical 7 ← moved
F-02 high low 0 ← moved
F-03 medium critical 6 ← moved
F-04 high medium 2 ← moved
F-05 medium critical 6 ← moved
F-06 high medium 2 ← moved
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.
{
"report": {
"summary": {"critical": 0, "high": 0, "medium": 0, "low": 0, "informational": 0},
"demonstrated": [
{"finding_id": "str", "severity": "critical|high|medium|low|informational",
"severity_inputs": {"reproduced": true, "auth": "none|user|admin", "sink": "str"},
"title": "str", "impact": "str", "observable": "str",
"fix": "str", "fix_cost": "low|medium|high"}
],
"asserted": [{"finding_id": "str", "severity": "str", "why_not_demonstrated": "str"}],
"scope": {"analysed": ["str"], "deferred": ["str"], "blind_spots": ["str"]},
"quality": {"validated": 0, "failed_to_reproduce": 0, "false_positive_rate": 0.0}
}
}
severity_inputs. Unarguable, therefore unactionable.-severity_rank, cwe, file, line) or two runs disagree and the diff between reports becomes unreadable.This is the end of the pipeline. Feed quality.false_positive_rate back into the next run's Phase 2 scoring — a pipeline that never learns from its own misses repeats them at machine speed.
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.15Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-15-severity-calibration:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.15 | type /c2-15-severity-calibration |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.15 | type /c2-15-severity-calibration in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.15 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.15 | type $c2-15-severity-calibration, 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.15Neither 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.15 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.15 --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.
Calibration moves several findings off their rule severity: the confirmed reachable CWE-89 that chains into account takeover becomes critical, while the unreachable and unvalidated ones fall. The top-3 by rule severity and by calibration disagree. The stage table shows review with the worst precision and highest minutes per finding, and design carrying the highest escape cost despite only two findings.
Recalculate severity for your current open findings using confirmation and reachability alone — you do not need chaining to see the effect. The queue reorders, and the items that fall are usually the ones people have been arguing about.
Tools used
Skill source: skills/appsec/appsec-triage-report