API Bill of Materials: The SBOM for your API surface
The market is bifurcated. Runtime tools (Salt Security, Akto) find shadow APIs after they're live in production traffic — for $100K/year. Spec-based tools (42Crunch) lint an OpenAPI spec that's usually stale. Nobody owns the gap: static, pre-deployment, code-level detection that runs in your CI pipeline for free.
shadowaudit v1.2.0 fills that gap with the API Bill of Materials (ABOM).
The problem
OWASP API9:2023 (Improper Inventory Management) is in the API Top 10 — and has been since 2019. Every SOC2, ISO 27001, and PCI audit opens with the same question: "give us a complete inventory of all API endpoints."
Teams answer with a 6-week manual spreadsheet exercise. A senior engineer walks the codebase, opens every routes/ folder, lists every @GetMapping, @app.route, Router.get, and resources :posts declaration, then copies them into a Google Sheet that's stale the moment the next PR merges. The auditor signs off. Three weeks later, an undocumented endpoint ships to production — because nobody updated the sheet.
This is the gap nobody owns. Runtime tools like Salt Security and Akto catch shadow APIs in production traffic, but they cost six figures a year and only see what's already deployed. Spec-based tools like 42Crunch lint an OpenAPI spec, but the spec is almost always stale — written once at project kickoff, never updated, and routinely missing half the actual endpoints. Neither runs in CI. Neither produces evidence an auditor can independently verify.
What is an ABOM?
The API Bill of Materials — ABOM — is the API equivalent of SBOM (Software Bill of Materials). Where an SBOM lists every dependency in your build, an ABOM lists every route in your API. The same principles apply:
- Machine-readable — JSON, versioned (
abom-1.0schema), diffable in git. - Cryptographically signed — ed25519, so an auditor can verify the manifest wasn't tampered with between CI and review.
- Generated at build time — never hand-maintained, never stale, regenerated on every commit.
- Per-deploy — one ABOM per deploy, stored alongside the artifact, replayable months later when an incident report demands "what was live on August 3rd?"
The schema is abom-1.0. Every manifest carries a schema version, a signedAt timestamp, the tool version that produced it, a full route inventory with per-route risk scores, and — when signed — an ed25519 signature over the canonicalized payload.
How it works
shadowaudit --dir ./src --abom
This walks your source tree, runs framework-aware scanners across all 8 supported frameworks (Express, FastAPI, Django, Flask, NestJS, Rails, Spring Boot, Fastify), and emits a JSON manifest at .shadowaudit/abom.json. The manifest contains:
- Full route inventory — method, path, source file, line number, handler name, detected middleware (auth yes/no), documentation status (matched against an OpenAPI spec if
--specis provided). - Per-route risk score — 0-100, weighted (see below).
- Stats summary — total routes, auth coverage, documentation coverage, severity band counts.
- Optional ed25519 signature — present if
--abom-signis passed.
Composite Risk Score
A boolean "undocumented" flag is useless for triage. A route that's undocumented AND has no auth AND accepts a DELETE on a sensitive path is a 4-alarm fire; a route that's undocumented but sits behind OAuth2 and only does GET /health is a paperwork issue. v1.2.0 ships a composite 0-100 risk score per route:
| Signal | Weight |
|---|---|
| Undocumented (no spec match) | +30 |
| No auth detected | +25 |
| Dynamic params / BOLA surface | +10 |
Sensitive path (/admin, /users, /payments, etc.) | +15 |
Destructive method (DELETE, PUT) | +10 |
| Undocumented AND unauthenticated (compound) | +10 |
Max possible score: 100. Severity bands: INFO (0-29), MEDIUM (30-49), HIGH (50-74), CRITICAL (75-100). The compound penalty for "undocumented AND unauthenticated" is what pushes the worst routes over the CRITICAL threshold — these are the routes an attacker would target first, because they're both invisible to defenders and reachable without credentials.
ABOM Diff Engine
shadowaudit --abom-diff old.json new.json compares two manifests and reports:
- Added routes — present in
new, absent inold. - Removed routes — present in
old, absent innew. - Auth-changed routes — same path, auth state flipped.
- Risk-changed routes — same path, risk score delta ≥ 10.
This turns the ABOM from a static inventory into a release-evidence artifact. Wire it into CI: generate an ABOM on every PR, diff against main's ABOM, post the delta as a PR comment. A reviewer sees at a glance: "this PR adds 3 routes, 2 of them undocumented, one with no auth." That's the review conversation that prevents shadow APIs from shipping in the first place — not a $100K/year runtime tool catching them in production three weeks later.
Signing
shadowaudit --dir ./src --abom --abom-sign key.pem --abom-output abom.json
The --abom-sign flag takes an ed25519 private key and signs the manifest. The signature covers the canonicalized route inventory, stats, and signedAt — anything an attacker might tamper with to hide a shadow route. verifyABOM() in the SDK checks the signature against the public key; if any byte of the route inventory changes after signing, verification fails.
The threat model: an engineer can't quietly delete a route from the manifest before sending it to the auditor. The auditor either has a valid signature (manifest is intact) or a verification failure (manifest was tampered with). No middle ground, no "trust me, I just cleaned it up."
Real-world validation
Before shipping v1.2.0, we ran the ABOM generator against three real-world codebases:
| Codebase | Framework | Routes detected | False positives |
|---|---|---|---|
| Spring PetClinic | Spring Boot | 17 | 0 (0%) |
| Express realworld | Express | 21 | 0 (0%) |
| Flask realworld | Flask | 19 | 0 (0%) |
Every detected route was verified against the source by file:line. Zero false positives across 57 routes. The ABOM's risk scores matched manual assessment: undocumented + no-auth routes scored 75-100 (CRITICAL), documented + authed routes scored 0-30 (INFO/MEDIUM). The composite scoring isn't theoretical — it tracks ground truth.
Brutal QA
19 adversarial tests ran against the ABOM pipeline before release. They found 4 critical bugs — all fixed before v1.2.0 shipped:
<int:id>normalization — Flask routes use converters like<int:user_id>. The spec normalizer was stripping them to empty strings, breaking spec matches and inflating "undocumented" counts. Fixed: converters now normalize to:user_idconsistently across inventory and matcher.signedAtpayload gap — the signature covered the route inventory but not thesignedAttimestamp. An attacker could backdate a manifest without invalidating the signature. Fixed:signedAtis now inside the signed payload.- Substring false positives —
/api/userswas matching/api/usersettingsin the spec matcher, marking the latter as "documented" when it wasn't. Fixed: path matching now requires exact normalized-path equality. - Case-sensitivity bugs —
GET /UsersandGET /userswere treated as distinct routes in the inventory but identical in the spec matcher, producing inconsistent risk scores. Fixed: both sides lowercase paths consistently before comparison.
Each bug had a regression test added. The 19-test suite now runs on every PR.
What's next
v1.2.0 is the foundation. The next three releases build on the ABOM:
- API Time Machine (v1.3.0) —
git blameintegration. Every route in the ABOM carries the commit that introduced it, the author, and the date. "Who added this shadow route, and when?" becomes a single command. - Compliance Evidence Engine (v1.4.0) — auditor-ready PDF reports generated from the ABOM. SOC2, ISO 27001, and PCI templates. One signed ABOM in, one signed report out.
- GitHub Action with README badge (v1.3.0) — drop-in CI integration. Every PR gets an ABOM diff comment; every README gets a badge showing the latest route count and risk distribution.
The ABOM is what the API security market has been missing: a free, static, CI-native artifact that produces verifiable evidence without a six-figure runtime subscription. Install it, run it on your repo, and send the signed manifest to your auditor.
npm install -g shadowaudit@1.2.0
shadowaudit --dir ./src --abom
← back to blog