Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/C2.7.mp4A clean dependency scan on an estate carrying an undeclared third-party binary reads as evidence of safety, and is evidence of nothing but the manifest's contents.
Reconcile the SBOM against what is on disk, then recover strings, imports and egress from the compiled artefact that no SBOM entry covers.
Scan an SBOM, then decompile the closed-source library it never mentions.
The test of a research programme is not what it discovered. It is what still protects you after the person who discovered it has left — which is a much smaller list, and a much more useful one.
appsec/supplychain.py reconciles requirements.txt against what your code actually imports, and reports two things a scanner will not: mcp is a declared dependency and a first-party package in your tree, and PyJWT is imported as jwt. Without the distribution-to-import map that second one is two findings, both false, from one correct manifest. Then it reads a .pyc for strings and egress, because there is no advisory for a file nobody catalogued.What a dependency scan actually says — nothing declared has a known vulnerability — and the two checks that say something about the system: reconciling the manifest against what the code imports, and reading the compiled artefact that no manifest entry covers.
A clean dependency scan on an estate carrying an undeclared third-party binary reads as evidence of safety.
Reconcile the SBOM against what is actually on disk, then recover strings, imports and egress from the binaries it missed.
Artefacts on disk that appear in the SBOM. The gap is the finding, and it is usually not zero.
Measure this
| what to measure | denominator or target |
|---|---|
| artefacts on disk that appear in the SBOM | share; the gap is the finding and is usually not zero |
durability ladder 5 control + eval case prevents it, and proves it stays prevented 4 regression case in CI fails the build when the finding returns 3 detection rule fires if the precondition recurs 2 written repro card someone else can reproduce it 1 slide deck survives; nobody re-runs it 0 chat thread gone at the next retention sweep only 4 and 5 survive the author leaving
Everything the pipeline has audited so far is code CyberTravels wrote. Most of what it ships is not.
A dependency scanner answers one question, and answers it well: are any of the components I declared known to be vulnerable? Both halves of that sentence are limits, and they are the whole subject of this lesson.
It sees only what was declared. An SBOM is generated from a manifest. A vendor's integration bundle that drops a jar into lib/ is in no manifest, so it is in no SBOM, so the scanner has no identifier to look up — and the report comes back clean. That report is accurate. It is a statement about the manifest, and it is being read as a statement about the build.
It sees only what is already published. "No advisory" means nobody has published one against that identifier yet. For an undeclared artefact there is no identifier at all, so the absence of an advisory carries no information whatsoever — and it renders identically on the report to a component that has genuinely been assessed.
So the procedure has three parts, and only the first is what most teams call software composition analysis:
1.9 < 1.10 is false as a string and true in every versioning scheme in use, and that one mistake drops findings silently.That third step is what jadx, procyon, Ghidra and strings all start with. The lesson runs it on a real compiled class and gets a hardcoded endpoint and a credential-shaped literal out of a library that appears in no manifest.
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 ``| a dependency scanner answers | it cannot answer | |
|---|---|---|
| declared | is this component vulnerable? | is anything here I did not declare? |
| published | is there an advisory? | is this artefact dangerous with no advisory? |
The bottom-right cell is where vendored SDKs, agent plugins and MCP server distributions live. Every one of them arrives as somebody else's compiled code inside your build, and none of them appears in a report that reads a manifest.
At CyberTravels the booking provider ships an integration bundle. It works, it was signed off by procurement, and nobody has ever opened it.
The scan below finds three real advisories in five declared components, and every word of it is correct. Then the reconciliation finds a sixth component that is on disk, in no manifest, and therefore in none of the three numbers above it.
The failure is not that the scanner is wrong. It is that "3 findings across 5 components" and "3 findings across 5 of 6 components, one unassessed" are the same report unless somebody counts the filesystem.
Three fixtures and one real artefact. The SBOM is CycloneDX, the advisory feed is OSV-shaped, the inventory is what an image scan returns — and VendorTelemetry.class is a genuinely compiled Java class, built with javac and committed beside the skill. The parser reads its constant pool the way a decompiler does and never sees the source.
The source is in the repository, in evidence/provenance/, so you can check the recovered strings against it. A vendor does not give you that, which is the reason the step exists.
https://telemetry.vendor-analytics.io/v2/ingest?k=vnd_live_8f2c41a09e7b
AES/ECB/PKCS5Padding
java.net.HttpURLConnection javax.crypto.Cipher
The endpoint and the key arrived concatenated — the compiler folded them into a single constant, which is a small gift and a real one: the artefact carries its own credential in plaintext, and no SBOM, CVE feed or licence scan would ever have shown you either.
Now the discipline that decides whether this finding survives review. What the constant pool proves is that this library can reach that endpoint and can encrypt before it does. What it sends is not in the constant pool. Write the finding as a capability and it is unarguable; write it as "exfiltrates traveller PII" and the one sentence you cannot evidence takes the rest of the report down with it.
AES/ECB is worth its own line, and it is the kind of thing this step finds by accident: ECB mode leaks structure across blocks, so whatever this is protecting, it is protecting it badly.
Three moves, in cost order:
The skill — skills/appsec/supply-chain-decompile/SKILL.md
name: supply-chain-decompile
description: >-
Scan an SBOM for known vulnerabilities, reconcile it against what is actually
on disk, and decompile the dependency that no manifest declares. Use when a
dependency scan comes back clean, when a third party ships a binary bundle,
or when asked what a closed-source library in the build actually does.
allowed-tools: Read, Glob, Grep, BashDependency scanning answers one question well: are any of the components I declared known to be vulnerable? Both halves of that sentence are limits. It sees only what was declared, and only what is already published as an advisory.
A vendor's integration bundle drops a jar into lib/. It is in no manifest, so it is in no SBOM, so no scanner has an identifier to look up and the report comes back clean. The clean report is accurate and it is about the manifest, not about the build.
The step past that is unglamorous and mechanical: reconcile the SBOM against the filesystem, and for anything on disk with no manifest entry, read the artefact itself. Compiled code carries its strings, its class references and its method references in a structured table, and recovering them needs no vendor cooperation and no source.
When any third party ships compiled artefacts into your build — SDKs, integration bundles, agent plugins, MCP server distributions. Also whenever a dependency scan is clean on a system that has never had a clean anything, which usually means the scanner is reading a manifest that stopped describing the build some time ago.
1 — Scan what was declared. Parse the SBOM, match every component against an advisory feed on (package, version) with a real range comparison, and report each hit with the fixed version. This is the part existing tools do, and it is worth doing first so the gap that follows is visible against it.
2 — Reconcile against the filesystem. List what is actually present in the built image and diff it against the SBOM's components. Two directions, and both are findings: on disk, not in the SBOM is undeclared code, and in the SBOM, not on disk means the SBOM describes a different build.
3 — For undeclared artefacts, read the artefact. No source, no manifest, no identifier to look up. A compiled class file carries a constant pool: every string literal, every class it references, every method it calls, in a documented table before any bytecode. Parse it — the format is stable and the parser is about forty lines.
4 — Read the recovered strings and calls as a capability claim. A URL and an HTTP client class in the same artefact is an egress capability. A crypto class beside them tells you what the traffic will look like on the wire. Report the capability, which is provable from the constant pool, rather than the intent, which is not.
5 — Say plainly what the scan could and could not have found. An undeclared artefact has no CVE because it has no identifier, not because it is safe. Those two produce identical output on a dependency report, and only one of them is a reason to relax.
Input — the fixture committed at the top of scripts/supply_chain_decompile.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:
SBOM: cybertravels-booking 4.7.1 · 5 declared components
CRITICAL org.apache.commons/commons-text 1.9 GHSA-599f-7c49-w659
Variable interpolation permits script execution (Text4Shell) -> fixed in 1.10.0
HIGH pyyaml 5.3.1 GHSA-8q59-q68h-6hv4
Arbitrary code execution via full_load / FullLoader -> fixed in 5.4
MEDIUM requests 2.31.0 GHSA-h5c8-rqwp-cp95
Proxy-Authorization header leaked on redirect -> fixed in 2.32.0
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.
{
"sbom": {"components": 0, "vulnerable": 0},
"advisories": [{"package": "str", "version": "str", "id": "str",
"severity": "str", "fixed": "str"}],
"reconciliation": {"undeclared_on_disk": ["str"], "declared_not_present": ["str"]},
"decompiled": [{"artefact": "str", "strings": ["str"], "classes": ["str"],
"capabilities": ["str"]}],
"unassessable_by_sbom": 0
}
unassessable_by_sbom is the number this whole procedure exists to produce. A report where it is absent has quietly claimed that undeclared code is vulnerability-free.
1.9 < 1.10 is false lexically and true in every version scheme in use. It silently drops the finding.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.7Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick c2-7-supply-chain:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons C2.7 | type /c2-7-supply-chain |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons C2.7 | type /c2-7-supply-chain in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons C2.7 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons C2.7 | type $c2-7-supply-chain, 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.7Neither 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.7 --out work/cybertravels
python3 scripts/checkpoint.py --at C2.7 --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.
Three of five declared components carry published advisories — Text4Shell in commons-text 1.9 as critical, PyYAML 5.3.1 as high, requests 2.31.0 as medium — and commons-text only appears because the version comparison is numeric rather than lexical. Reconciliation then finds one artefact on disk that is in no manifest, which every one of those numbers excluded. Decompiling it recovers, with no source, a hardcoded telemetry endpoint with the licence key folded into the same literal, an AES/ECB cipher spec, and references to HttpURLConnection and javax.crypto.Cipher — network egress and cryptography as provable capabilities. unassessable_by_sbom: 1 is the number the report exists to produce.
Generate an SBOM for one service you run, then list what is actually in the built image and diff the two. Every artefact on disk with no manifest entry has been scanned by nothing, and the clean report you have been reading each week was never about it. For the first one you find, pull the strings out of it before you ask the vendor — the conversation goes differently when you open with the endpoint.
Skill source: skills/appsec/supply-chain-decompile