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

Severity calibration, triaging and reporting

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.15.mp4
Risk

Severity is a label copied from the rule, so the queue is ordered by something that predicts nothing.

Control

Stage 15: calibrate severity from sandbox evidence, then report per-stage economics rather than a finding count.

The lab

Recalculate severity from confirmed exploitation and reachability, then produce the per-stage escape economics.

◉Use case relevance

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.

At CyberTravels. 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.

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

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.

Day 0 — why

Severity copied from the rule orders the queue by something that predicts nothing.

Day 1 — how

Calibrate severity from sandbox evidence, and report per-stage economics rather than a single total.

Day 2 — measure

Cost per confirmed finding at each stage, which is what tells you which stage to invest in next.

Measure this

what to measuredenominator or target
cost per confirmed finding, per stagecurrency per finding; it is what says which stage to invest in next

▦The framework, and how it works

   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 both

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

  • did stage 12 confirm it by execution?
  • is it reachable from an untrusted entry point (stage 10)?
  • what does it chain into (stage 13)?
  • does it sit in a historical risk zone (stage 1)?

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 ``

2 · Stage 15 — calibrate severity from what the pipeline learned

3 · Where it breaks — rule severity as the queue order

4 · The report — per-stage economics, not a finding count

5 · Stage 15 as a skill — severity you can argue with

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.

6 · Where it breaks — the uncalibrated headline

Now report the same findings without the cap.

▶Real time execution as skill

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

AppSec pipeline · Phase 5 — Reporting

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

When to use this

At the end of a review, or whenever findings must be handed to someone who did not do the analysis.

Inputs

findings (Phase 3), validations (Phase 4), and plan.deferred (Phase 2). The last one is not optional — it is the scope statement.

Procedure

Calibrate severity, and show the calibration. Severity is a function of impact and reachability, and both are already computed upstream. State them:

SeverityRequires
Criticalreproduced, unauthenticated path, sink is subprocess/deserialisation/credential
Highreproduced, or confirmed with an authenticated path to a dangerous sink
Mediumconfirmed, feasible, but mitigated in depth or requires elevated access
Lowconfirmed, not feasible in this deployment
Informationalneeds_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.

Example

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.

Output contract

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

Failure modes

  • Severity without severity_inputs. Unarguable, therefore unactionable.
  • Burying non-reproduction. It belongs in the finding, not the appendix.
  • Sorting by severity alone. Ties must break deterministically (-severity_rank, cwe, file, line) or two runs disagree and the diff between reports becomes unreadable.
  • Counting conformance as quality. See above; it is the most common way an automated pipeline flatters itself.

Handoff

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.

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

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-15-severity-calibration:

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

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

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.

✎Your turn

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

OpenGrepGLM-4.6Claude Haiku 4.5

Skill source: skills/appsec/appsec-triage-report

← PreviousC2.14 · Bonus — testing safely: the controls an offensive agent runs inside