shorproof
v0.2.0
Published
Post-quantum readiness scanner for JavaScript/TypeScript. Finds Shor-breakable crypto (RSA, ECDSA, ECDH) in source, dependencies, JWT/JWKS and PEM/X.509 artifacts — then gives you an ordered migration plan, a crypto-agility report, and a live OpenID Conne
Maintainers
Keywords
Readme
shorproof
Is your code Shor-proof? A post-quantum readiness scanner for JavaScript & TypeScript.
shorproof finds the quantum-vulnerable cryptography in your project — the RSA, ECDSA, ECDH and elliptic-curve usage that Shor's algorithm breaks once large-scale quantum computers arrive — and points you to the NIST post-quantum replacements (ML-KEM / FIPS 203, ML-DSA / FIPS 204).
It scans source code, dependencies, JWT/JWKS, PEM/X.509 key material, and live OpenID Connect issuers — then tells you what order to fix it in and how many places you'd have to change. Findings come out as text, JSON, SARIF (GitHub code scanning), a CycloneDX 1.6 CBOM, an ordered migration plan, or a crypto-agility report.
Why it's different: auth/JWT-first, binding-aware (it tracks the real import, not a variable named jwt), near-zero dependency (exactly two), and built so every finding survives review by a cryptographer — no fear-mongering, no false positives by design. It even tells you what you've already done right: ML-KEM / ML-DSA usage is reported as safe ✓.
What's new in 0.2.0
0.1.0 could tell you what quantum-vulnerable cryptography you had. 0.2.0 is about what to do with it.
| | |
| --- | --- |
| --format plan | An ordered migration plan. Signature work has a real trap — switch a signer to ML-DSA before every verifier accepts it and every token in production is rejected — so the plan is ordered around it. |
| --format agility | How many distinct places you'd actually have to edit. A count of edit points, not a score. |
| shorproof oidc <issuer> | Audit live OpenID Connect issuers — discovery document, JWKS, advertised algorithms. |
| Scan progress | A "collapse" display for long scans, on stderr, so stdout stays byte-identical. |
| Confirmed usage wins | A dependency says what a project can do; a call site says what it does. When source confirms usage, the manifest entry steps aside — so a service signing only with ML-DSA now reports nothing vulnerable. |
Your finding counts will move, mostly downwards. See the upgrade notes.
Quick start
# No install needed
npx shorproof # scan the current project
npx shorproof ./api # scan a specific directory
# Audit a live OpenID Connect issuer (the only command that uses the network)
npx shorproof oidc https://acme.auth0.com
# Output formats
npx shorproof --json # stable machine-readable JSON
npx shorproof --format sarif # SARIF 2.1.0 for GitHub code scanning
npx shorproof --format cbom # CycloneDX 1.6 Cryptographic BOM
npx shorproof --format plan # ordered Markdown migration plan
npx shorproof --format agility # how many places you'd have to edit
# CI gating
npx shorproof --fail-on high # exit 1 on any high+ finding
npx shorproof --strict # shorthand for --fail-on high
# Display
npx shorproof --no-progress # no scan progress display
npx shorproof --no-color # no ANSI color
# Scope
npx shorproof --ignore build # skip a directory name (repeatable)node_modules, .git, dist and the framework build/cache directories
(.next, .nuxt, .output, .svelte-kit, .angular, .astro, .vercel,
.netlify, .turbo, .parcel-cache, .cache, coverage) are always skipped —
build output is generated code, and a finding there can't be fixed at that
location. build and out are not skipped by default, since they're real
source in some projects; pass --ignore if they're output in yours.
Or add it as a dev dependency:
npm install --save-dev shorproofRequires Node.js ≥ 20.12 (tested on 20, 22, 24).
Example
shorproof v0.2.0 — post-quantum readiness scanner
Scanned /srv/api
CRITICAL (1)
keys/tls.key RSA-2048
keys/tls.key:1
RSA is broken by Shor's algorithm… This is stored private-key material.
→ Migrate to ML-DSA (FIPS 204) for signatures and ML-KEM (FIPS 203)…
HIGH (2)
sign(payload, key, { algorithm: 'RS256' }) RS256
src/auth/token.ts:42
RS256 signs with RSA, which Shor's algorithm breaks…
→ ML-DSA via jose ≥ v6 or Node 24.7+
SAFE (1)
new SignJWT(...).setProtectedHeader({ alg: 'ML-DSA-65' }) ML-DSA-65
src/auth/pq.ts:8
ML-DSA (FIPS 204) is a NIST post-quantum signature standard — already quantum-safe.
1 critical · 2 high · 1 safeWhat it scans
| Scanner | Looks at | How |
| --- | --- | --- |
| deps | every package.json in the tree | curated knowledge base of crypto packages — a monorepo with no root manifest is scanned, not silently skipped |
| ast | JS/TS source | Babel AST, binding-aware — keys off the resolved import (any alias, require, namespace, dynamic import()), and confirms a vulnerable usage before reporting |
| artifacts | JWKS/JWK, PEM keys, X.509 certs | parsed with native node:crypto; certs valid past ~2030 are elevated |
| oidc | live OpenID Connect issuers | opt-in subcommand — discovery doc + jwks_uri, classified by the same code as a local JWKS |
The AST scanner covers Node crypto (sign/verify, generateKeyPair, ECDH, DH, publicEncrypt/privateDecrypt), WebCrypto crypto.subtle.*, and the JWT stack — jsonwebtoken, jose (incl. the SignJWT builder) and express-jwt — resolving algorithm options by constant propagation and recognizing ML-DSA-44/65/87 as already post-quantum. Importing a crypto library is never, by itself, a finding.
Confirmed usage supersedes the manifest. A dependency is a claim about what a project can do; a call site is a fact about what it does. When the source scanner confirms usage of a package, its manifest entry drops to info and the severity stays with the call sites that earned it — so a service signing only with ML-DSA-65 reports nothing vulnerable, instead of being told to "check which alg values you actually use" one line above a safe finding naming the one it uses. The entry is downgraded, never removed: the package is still installed, a file may have failed to parse, and absence of evidence isn't proof. Packages the scanner has no rules for — where there is no ground truth — are untouched, and in a monorepo evidence never crosses package boundaries: one service's confirmed usage cannot vouch for its sibling's manifest.
A documented default counts as confirmed usage. jwt.sign(payload, key, { expiresIn }) names no algorithm, and jsonwebtoken signs with HS256 — its own sign.js reads alg: options.algorithm || 'HS256', and it rejects a key that doesn't match. That's reported safe, not passed over: staying silent means nothing confirms the package, so its manifest entry keeps full severity and the report warns about a library whose real use is provably fine. verify gets no such treatment — without algorithms it infers the set from the key type, which can't be read statically.
Test code is marked, never re-scored. An RSA key generated in a unit test is still RSA, so the finding stands at its real severity — but calling a throwaway test key "exposed to harvest-now-decrypt-later" is a sentence that isn't true, and one untrue sentence costs a report its credibility. Findings under __tests__, test/, spec/, e2e/, *.test.* and friends carry context: "test" and are marked [test code] in the report. Re-scoring by path would be a slope with no bottom; a marker tells the reader what they need without the tool pretending to know more.
GitHub Action (code scanning)
Upload SARIF so findings show up in your repo's Security → Code scanning tab and on PRs. A ready-to-copy workflow lives in examples/github-action.yml:
name: shorproof
on: [push, pull_request]
permissions:
security-events: write # required to upload SARIF
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npx [email protected] . --format sarif > shorproof.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: shorproof.sarifUsing shorproof with AI coding agents
shorproof is built to be driven by an AI coding agent (Claude Code, Cursor, Copilot, Aider, …), not just a human at a terminal. When you ask your agent something like "check this project for quantum-vulnerable crypto", it can run one command and parse a stable, machine-readable result — no plugin, no API key, no config.
npx shorproof . --jsonThe --json output is a documented, stable schema (see JSON schema) — an agent can read summary for a verdict, walk findings for each severity / file / line / why / migration, and check skipped for anything it couldn't analyze. For CI-style gating an agent can rely on the exit code (--fail-on high → exit 1).
A prompt that works well:
Run
npx shorproof . --json, then summarize the quantum-vulnerable crypto by severity. For eachhigh/criticalfinding, show the file:line and themigrationhint. Don't touch anything markedsafe— that's already post-quantum.
Because detection is binding-aware and usage-confirmed, the agent gets signal, not noise: importing a JWT library isn't a finding, ML-DSA usage comes back safe, and a stray "RS256" in a comment is ignored. That keeps the agent from "fixing" things that aren't broken. A one-page machine-readable capability summary also lives at llms.txt.
Severity philosophy (the honest part)
Severity turns on lifetime and harvest-now-decrypt-later (HNDL) exposure, not on drama. Equal Shor-breakability can carry different severity:
- critical — Shor-breakable crypto protecting long-lived secrets/signatures: encryption of stored data, certs valid past 2030, JWKS keys, private-key files.
- high — Shor-breakable usage with typical exposure: JWT signing (RS/ES/EdDSA), TLS-adjacent key material.
- medium — quantum-weakened but manageable (AES-128 via Grover — use AES-256), or classically weak already (MD5/SHA-1 — said honestly: broken today, not a quantum issue). Bare EC key generation lands here.
- review — a crypto-capable surface whose usage couldn't be confirmed statically.
- safe — AES-256, SHA-256/SHA-3, bcrypt/argon2, and ML-KEM/ML-DSA/SLH-DSA — positively reported as "already post-quantum ✓".
SHA-256 is never flagged as a risk, and symmetric crypto is never implied to be Shor-broken. Every finding's why is one honest sentence a cryptographer would sign off on.
Output formats
- text (default) — grouped by severity with file:line, the honest
why, and a concrete migration hint. Colors auto-disable on non-TTY /NO_COLOR. - json — stable, documented schema (below); treat it as a public API.
- sarif — SARIF 2.1.0 so GitHub code scanning renders findings on PRs. Severity maps to SARIF level and GitHub's
security-severity; post-quantum/inventory findings are informational, not alerts. - cbom — CycloneDX 1.6 Cryptographic Bill of Materials: a full inventory (vulnerable and post-quantum alike) with
algorithmPropertiesand NIST quantum security levels. Validated against the CycloneDX 1.6 schema. - plan — an ordered Markdown migration plan (see above).
- agility — how many places you'd have to edit (see above).
Auditing live issuers (shorproof oidc)
Which of your identity providers still signs everything with RS256? Point shorproof at them:
npx shorproof oidc https://acme.auth0.com https://login.acme.comIt reads each issuer's /.well-known/openid-configuration, follows jwks_uri,
and classifies the published keys and the advertised signing algorithms —
with the same code that classifies a jwks.json on disk, so a deployed issuer
and a committed key file get the same verdict. kty: AKP (RFC 9964, ML-DSA)
comes back safe ✓. Every output format works, including --format plan, and
--fail-on gates CI as usual.
An advertised algorithm is treated as a capability, not a usage — the same
rule that keeps a crypto library in package.json from being a finding on its
own. An endpoint offering only Shor-breakable algorithms is confirmed (no client
configuration can avoid it); an endpoint that also offers a quantum-safe option
is review, because which one your tokens use depends on configuration this
scan cannot see. That verdict is computed per endpoint, so an issuer whose
request objects accept ML-DSA cannot mask ID tokens that are still RSA-only.
About the network. This is the only command that makes an outbound request.
A plain shorproof scan never does — it doesn't even load the module that
could. It fetches only URLs you type: there is deliberately no "find issuer URLs
in the source and audit them" mode, because fetching URLs discovered inside
files is how a scanner becomes an SSRF vector in someone's CI. Requests are
bounded by scheme (https only, plain http for localhost alone — re-checked
after every redirect), a 10s timeout, a 1MB body cap, and 3 redirect hops. One
unreachable issuer never hides what the others said.
Migration plan (--format plan)
A findings list tells you what. It doesn't tell you in what order — and signature migration has an ordering trap that costs you an outage:
Switch your signer to ML-DSA before every verifier accepts it, and every token in production is rejected.
--format plan emits an ordered Markdown plan you can paste straight into an
issue or a PR:
npx shorproof --format plan > MIGRATION.mdTrack A — signatures is ordered, because of the trap above: teach every verifier both algorithms → create and publish the new key → switch signing → retire the classical key. Each step lists the exact sites, grouped by call shape, and carries a short recipe with its RFC/FIPS references.
Track B — encryption and key agreement is not ordered: there's no deadlock to avoid, but the clock started when the traffic was recorded, not when you start migrating.
The ordering is derived, not guessed. Every finding carries a role
(verify / keygen / keystore / publish / sign / encrypt /
inventory) — which is why jsonwebtoken.sign and jsonwebtoken.verify end
up in different steps even though they share a rule id.
What the plan will never do: invent effort estimates or timelines, drop a step
because this repo happened to contain no evidence for it (your verifiers may
live in another service), put an unresolved review finding into a confident
step, or tell you to migrate something that is already safe.
Crypto agility (--format agility)
Every migration meeting opens with "how long will this take?" — and no scanner has been measuring the thing that decides the answer. These three are identically quantum-vulnerable, and cost wildly different amounts to migrate:
sign(p, k, { algorithm: 'RS256' }); // 14 sites → 14 edits
sign(p, k, { algorithm: SIGNING_ALG }); // 14 sites → 1 edit
sign(p, k, { algorithm: process.env.JWT_ALG }); // 14 sites → 0 editsnpx shorproof --format agility| Where the algorithm comes from | Sites | Edit points | Cost |
| inline at the call site | 2 | 2 | one edit per site |
| a module-level const | 2 | 1 | one edit per definition|
| chosen at run time | 2 | 0 | no code edit |
**Blast radius: 4 edit points across 3 files.**Each call site is classified by where its algorithm actually comes from —
inline, local, module, imported, config, dynamic — and every level is
an AST fact, never a guess about what an identifier is named: a parameter is
a parameter, an import is an import, process.env.X is process.env.X.
Blast radius is a count of distinct edit points, not a score. No weights, nothing tunable. Call sites sharing one constant collapse to one edit — that collapse is the whole measurement. It is an honest lower bound and says so: imported and computed values can't be located, and configuration-driven ones need no edit at all, so they're excluded rather than counted as free work.
Agility is not a severity. It never reaches --fail-on, SARIF alerts or the
CBOM: "hard to change" and "dangerous" are different questions. Findings with no
algorithm choice in code (a dependency, a key file) carry no agility at all.
--format plan uses the same data — steps read 10 sites · 7 edit points.
Scan progress
An interactive run shows a band of cells in superposition that collapses into solid blocks as files are finished:
⟨███▒░▓▒░▓░▒▓⟩ ast 124/380 files 0.9sThe two halves mean different things on purpose. The shimmer moves every
frame and says only "still running". The collapse moves only when files are
genuinely finished — done / total, never a timer, never an easing curve — and
while the file total is still unknown, nothing collapses at all, because there
is no honest fraction to draw yet. A bar that drifts forward on a clock is
lying about work it has not done; this tool doesn't do that with findings, and
it won't do it with a progress bar either.
It stays out of the way: nothing is drawn for a scan that finishes in under
120ms, the animation is written to stderr so --json/--format sarif
piping is untouched, and it is skipped entirely when output is piped, in CI, or
on TERM=dumb. Disable it with --no-progress or SHORPROOF_PROGRESS=0; force
it on in a terminal that reports itself badly with SHORPROOF_PROGRESS=1; force
the ASCII glyph set with SHORPROOF_ASCII=1.
JSON schema
{
"tool": "shorproof",
"version": "0.2.0",
"root": "/abs/scan/root",
"scanners": ["deps", "ast", "artifacts"],
"summary": { "critical": 0, "high": 2, "medium": 1, "review": 0, "safe": 1, "info": 0 },
"findings": [
{
"source": "ast", // "deps" | "ast" | "artifact"
"ruleId": "jsonwebtoken/rs256",
"severity": "high",
"category": "signature", // signature|kem|key-exchange|hash|symmetric|artifact
// Where this sits in a migration. Drives --format plan; also lets an
// agent tell a signer from a verifier, which the ruleId cannot.
"role": "sign", // verify|keygen|keystore|publish|sign|encrypt|inventory
// AST findings only, and only where an algorithm was chosen in code:
// where that choice lives, and therefore what changing it costs.
"agility": { "source": "module", "line": 4, "name": "SIGNING_ALG" },
// Present only for findings in test code. Never affects severity.
"context": "test",
"algorithm": "RS256",
"title": "…", "why": "…", "migration": "…",
"confidence": "high", // high|medium|low
"lifetimeSensitive": true,
"location": { "file": "src/auth.ts", "line": 12, "column": 3 }
// source-specific: deps → package,range | ast → snippet | artifact → detail
}
],
// Files that could not be analyzed — unparseable, or a traversal error.
// Reported, never silently dropped; empty array when none.
"skipped": [{ "file": "vendor/bundle.js", "reason": "analysis error: Duplicate declaration \"x\"" }]
}A file that can't be parsed, or whose AST traversal throws (e.g. a duplicate declaration in a vendored or concatenated bundle), is recorded in skipped and surfaced — in the text footer and this JSON array — never silently dropped, and never aborting the scan. One un-analyzable file must not hide the findings in the rest of the tree.
Exit codes
0— clean, or findings reported without a--fail-on/--strictthreshold.1— a finding at or above the--fail-onseverity (--strict=--fail-on high).safe/infonever fail a run.2— usage or I/O error (bad directory, unknown--format/--fail-on).
Why "shorproof"?
Peter Shor's 1994 algorithm is the reason post-quantum cryptography exists: on a large enough quantum computer it breaks RSA, Diffie-Hellman and elliptic-curve crypto — the math behind most of today's TLS, JWTs and signatures. NIST's replacement standards (FIPS 203/204/205) are final, migration deadlines are set (RSA/ECC deprecated ~2030, disallowed ~2035), and every migration starts with knowing where your vulnerable crypto lives.
That first step is what this tool is for. Make your code Shor-proof.
Changelog
See CHANGELOG.md.
License
MIT © Usama Amjid
