@quantakrypto/qscan
v0.10.0
Published
qScan — find quantum-vulnerable cryptography in any codebase (CLI). Zero runtime dependencies.
Readme
@quantakrypto/qscan
Find quantum-vulnerable cryptography in any codebase.
qscan walks a project and reports where it relies on classical asymmetric
cryptography (RSA, (EC)DH, ECDSA, EdDSA, DSA, …) — the algorithms broken by a
sufficiently large quantum computer, and the ones exposed to harvest-now,
decrypt-later (HNDL) attacks today. It scans source files, dependency
manifests, and configuration, then prints a readiness score and a concrete next
step.
- Zero runtime dependencies. Node built-ins only; the engine is
@quantakrypto/core. - CI-friendly. Severity thresholds drive the exit code; baselines suppress known findings so the build only fails on new problems.
- Multiple formats.
human(default),json, SARIF 2.1.0 for code-scanning dashboards, a CycloneDX 1.6 CBOM for compliance tooling, an ISO 27001 A.8.24 evidence report, and an OpenVEX 0.2.0 document for VEX pipelines. - Fast on big repos. Optional worker-thread parallelism (
--parallel) and git-aware incremental scanning (--changed).
Language coverage
Inline crypto detection currently covers JavaScript/TypeScript, Python,
Go, Java/Kotlin/Scala (JCA + BouncyCastle), C#/.NET, Rust, Ruby,
PHP (openssl / phpseclib3 / libsodium), Elixir (:crypto / X509 / JOSE),
C/C++ (OpenSSL, Mbed TLS, wolfSSL), Swift (CryptoKit / Security),
Objective-C (Security SecKey*), and Dart/Flutter (pointycastle /
cryptography), and smart contracts (Solidity/Move/Cairo on-chain
signature verification) source — 14 languages. PEM key material, SSH keys (including
SSH-CA certificates), TLS/certificate config, reverse-proxy/gRPC TLS, WebAuthn/FIDO2,
code-signing, weak signature hashes (SHA-1/MD5), DKIM, SPIFFE/SPIRE, and dependency
manifests for eight ecosystems — npm (plus yarn.lock / pnpm-lock.yaml),
PyPI, Cargo, Go modules, Maven, RubyGems, NuGet, and Composer — are detected in any
file regardless of language.
qScan is honest about coverage: if a scan walks files but finds none in a
supported source language, it says so and will not present a bare 100/100
as a clean bill of health — the crypto may simply live in a language it can't
read yet. Always check the analyzed count in the report before relying on the
score.
Install
npm install -g @quantakrypto/qscan
# or run without installing
npx @quantakrypto/qscan .Requires Node ≥ 20.
Usage
qscan [path] [options]path defaults to the current directory (.).
Options
| Flag | Description | Default |
| --- | --- | --- |
| --format <human\|json\|sarif\|cbom\|evidence\|vex> | Output format. | human |
| --cbom | Alias for --format cbom (CycloneDX 1.6 CBOM). | — |
| -o, --output <file> | Write the report to a file instead of stdout. | stdout |
| --severity-threshold <level> | Exit 1 if any finding is at/above this level. One of critical, high, medium, low, info. | high |
| --no-source | Skip scanning source files for inline crypto. | scan on |
| --no-deps | Skip scanning dependency manifests. | scan on |
| --no-config | Skip scanning config files (TLS/certificates). Toggles config detectors — not the config file below. | scan on |
| --config <path> | Use this quantakrypto.config.json instead of auto-discovering one at the scan root. | auto |
| --no-config-file | Disable quantakrypto.config.json auto-discovery. | discovery on |
| --ignore <pattern> | Exclude paths matching <pattern>. Repeatable. | — |
| --include <pattern> | Restrict the scan to paths matching <pattern>. Repeatable. | all files |
| --max-file-size <bytes> | Skip files larger than <bytes>. | 2 MiB |
| --no-default-ignores | Don't skip node_modules/.git/dist by default. | ignores on |
| --scan-minified | Scan minified/generated/bundled files too. | skipped |
| --changed | Incremental: scan only files git reports as changed. | off |
| --since <git-ref> | With --changed, diff against <git-ref> (implies --changed). | working tree |
| --parallel | Scan using a worker-thread pool when the workload is large enough. | off |
| --concurrency <n> | Worker count for --parallel (implies --parallel). 0 forces serial. | CPU count |
| --audit | Opt-in supply-chain checks (see below): dependency advisories via each ecosystem's own audit tool, plus a declared-source-repository (provenance) check. Findings merge into the report and the exit code; a missing tool or network hiccup degrades to a stderr diagnostic. | off |
| --mandate <id> | Gate findings against a compliance mandate's dated clauses (cnsa-2.0, nist-ir-8547; repeatable). Reports each prohibited finding with its clause + deadline; fails the build only once a disallow deadline has passed. Verdicts also ride in --format json (mandateMapping), sarif (run.properties.mandate), and evidence (hashed). | off |
| --lead-months <n> | Fail early when a --mandate disallow deadline is within n months. | — |
| --fail-now | Fail on any --mandate-prohibited finding, ignoring the deadline. | off |
| --policy <file> | Org crypto-policy JSON: adds §4 verdicts to --format evidence; with --mandate, permitted/in-transition families are acknowledged and exempt from the early gate (--fail-now/--lead-months), but a passed disallow deadline still fails. | — |
| --profile <id> | Standards regime for the migration-targets footer: nist, cnsa-2.0, bsi-tr-02102, anssi, uk-ncsc. | nist |
| --tier <id> | CNSA security tier for migration targets: category-3, category-5 (alias for a profile). | — |
| --hndl | Score findings by harvest-now-decrypt-later exposure (reads hndl.yml; qscan hndl init scaffolds one). Annotates only; never changes the exit code. | off |
| --triage | BYOK LLM pass that re-ranks findings by real exposure and explains them. Never suppresses; never changes the exit code. Needs an API key. | off |
| --triage-floor <level> | With --triage, only triage findings at/above this level. | medium |
| --context <level> | How much source is shared with the LLM: metadata, snippet, function, file (key material redacted, best-effort — review with --dry-run). | snippet |
| --dry-run | With --triage, print the exact payload that would be sent and exit without contacting the provider. | off |
| --llm-provider <name> | BYOK provider: anthropic or openai-compatible. | anthropic |
| --llm-model <id> | Model id for the BYOK provider. | provider default |
| --baseline <file> | Suppress findings whose fingerprint is in the baseline file (severity gate only — the --mandate gate is evaluated on pre-baseline findings, so a baseline can't waive a regulatory deadline). | — |
| --write-baseline <file> | Write current findings as a baseline, then exit 0. | — |
| --quiet | Suppress the human summary banner. | off |
| -v, --version | Print version and exit. | — |
| -h, --help | Print help and exit. | — |
This table covers the common flags. Run qscan --help for the authoritative,
complete list (including --sign/--timestamp, --crypto-agility, --merge,
--cache, --top, --max-findings, --no-snippets, --color), plus the
subcommands qscan hndl init and qscan crypto-agility emit|validate.
Exit codes
| Code | Meaning |
| --- | --- |
| 0 | No findings at/above the threshold — or a baseline was written. |
| 1 | One or more findings at/above the severity threshold. |
| 2 | Usage error or I/O failure. |
Config file (quantakrypto.config.json)
qScan auto-discovers a quantakrypto.config.json at the scan root and applies it
under a strict precedence: CLI flags > config file > built-in defaults
(per-key). It encodes include / exclude / maxFileSize / noDefaultIgnores
/ scanMinified, the detector-family toggles, severityThreshold, and a
baseline path so CI and local runs share one policy. Point at a file elsewhere
with --config <path>, or disable discovery with --no-config-file. See
docs/CONFIG.md for the full schema.
Example output
qScan — quantum-vulnerable cryptography report
root: ./examples/vulnerable-app • files scanned: 2 • qscan v0.9.0
3 findings (2 high, 1 medium)
2 exposed to harvest-now-decrypt-later (HNDL).
Readiness score: 70/100
Top findings
high rsa-keygen src/crypto.js:5
RSA is not quantum-safe.
→ Use ML-KEM-768 (hybrid X25519MLKEM768).
high dep-vulnerable package.json:7
Dependency "node-forge" provides classical asymmetric crypto.
medium ecdh-usage src/crypto.js:13
ECDH is not quantum-safe.
→ Use a hybrid KEM (X25519MLKEM768).
Next step: migrate src/crypto.js — Use ML-KEM-768 (hybrid X25519MLKEM768).Color is emitted only when writing the human format to an interactive terminal
(and is suppressed by the NO_COLOR environment variable). Reports written to a
file or piped are always plain text.
Baselines
A baseline records the fingerprints of findings you have already triaged.
qScan uses the canonical baseline shared across the whole monorepo
(@quantakrypto/core, the GitHub Action, and this CLI): a single on-disk format and a
single fingerprint algorithm, so a baseline written by one tool is understood by
the others.
A fingerprint is a full SHA-256 of ruleId|file|normalizedSnippet. It is
deliberately line-insensitive — unrelated edits that shift line numbers no
longer invalidate it — and the snippet's whitespace is normalized so
reformatting doesn't either. It ignores volatile fields like severity wording or
timestamps.
# 1. Accept the current state of the world.
qscan . --write-baseline qscan-baseline.json
# 2. From now on, fail only on findings that are NOT in the baseline.
qscan . --baseline qscan-baseline.jsonThe baseline file is plain JSON:
{
"version": 1,
"fingerprints": [
"0f1e2d3c4b5a...full-sha256...",
"a1b2c3d4e5f6...full-sha256..."
]
}Incremental scans
In CI you usually only care about what a change introduced. --changed restricts
the scan to the files git reports as modified, which is a large win on big repos:
# Files modified in the working tree (staged, unstaged, and untracked).
qscan . --changed
# Everything that changed since a base ref (e.g. a PR base).
qscan . --changed --since origin/mainOutside a git work tree the changed-file list is empty (nothing is scanned), rather than an error.
Parallel scans
For large trees, route the scan through a worker-thread pool. qScan automatically
stays serial for small inputs, so --parallel is safe to leave on:
qscan . --parallel # auto worker count (CPU cores)
qscan . --concurrency 4 # pin the worker count (implies --parallel)Supply-chain audit (--audit, opt-in)
--audit layers three supply-chain checks on top of the normal scan. It is
opt-in because two of them shell out (dependency advisories) or make a network
request (provenance); a default qscan . stays offline. Every check degrades
gracefully — a missing tool, an unparseable output, or a network hiccup becomes
a qscan: audit: … diagnostic on stderr, never a hard failure. Any findings it
does produce merge into the report and the exit code, so a known-vulnerable
dependency can gate CI.
qscan . --audit # add advisory + provenance checks
qscan . --audit --format sarif -o qscan.sarifDependency advisories. For each ecosystem present at the scan root, qScan runs that ecosystem's own audit tool and turns its advisories into
dependencyfindings (ruleId: dep-advisory, severity from the advisory,<pkg>@<ver>: <summary> (<ID>), remediation = the patched version):- Rust —
Cargo.toml/Cargo.lock→cargo audit --json - Python —
requirements*.txt/pyproject.toml→pip-audit --format json - npm —
package-lock.json→npm audit --json
The tool must be on
PATH; if it is not, that ecosystem is skipped with a diagnostic (cargo audit not available, skipped). This complements the built-in quantum-vulnerable-dependency database (which flags packages whose purpose is classical public-key crypto) with known-CVE coverage.- Rust —
PQC parameter / size verification (a default detector, always on — not gated behind
--audit). Flags code that has already reached for a post-quantum KEM but got it subtly wrong: a pre-standard round-3 Kyber (pqc_kyber,pqcrypto-kyber,Kyber768, …) used where FIPS 203 ML-KEM is intended (pqc-prestandard-kem, confidence raised when a FIPS-203/ML-KEM/NIST claim is in the same file), and an ML-KEM/Kyber byte size that names a different parameter set than the code advertises (pqc-parameter-mismatch, conservative — fires only on an exact size against a conflicting advertised level).Provenance / declared source repository. Reads the root manifest's repository URL (
package.jsonrepository,Cargo.tomlrepository,pyproject.toml[project.urls]). A manifest that declares no repository yields an info finding (provenance-repo-missing); with--audit, a declared URL that 404s or does not resolve yieldsprovenance-repo-unresolved(medium). The HEAD request is the only network call qScan itself makes; transient network errors are skipped silently.
CBOM (CycloneDX)
Emit a CycloneDX 1.6 cryptographic bill of materials — one
cryptographic-asset component per distinct (assetType, algorithm, discriminator),
with file:line occurrence evidence — for compliance and supply-chain tooling.
Findings are classified into their proper CycloneDX assetType: algorithm
(crypto usage), certificate (X.509), related-crypto-material (private/public
key material), and protocol (TLS):
qscan . --cbom -o qscan-cbom.json
# equivalently:
qscan . --format cbom -o qscan-cbom.jsonThe output is deterministic (sorted components and occurrences, stable serial number), so re-running on an unchanged tree produces byte-identical CBOMs.
VEX (OpenVEX)
Emit an OpenVEX 0.2.0 document so the quantum-readiness posture flows into the same supply-chain pipeline that already ingests CVE-based VEX:
qscan . --format vex -o qscan.openvex.jsonOne statement per rule (a synthetic QK-<ruleId> vulnerability), listing every
affected file:line product with status: "affected" and the rule's remediation
as the action_statement. PQC findings have no CVE, so qScan mints a stable
per-rule identifier rather than claiming one. qScan never reports not_affected
— only an operator can attest a mitigation — so downgrading a statement is left to
you to post-process. When --triage is also set, each verdict (exposure score /
priority / rationale) is carried in the statement's status_notes. Output is
deterministic (statements sorted by vulnerability, products deduped and sorted;
the @id derives from the finding set).
Triage (opt-in, BYOK)
--triage adds an optional LLM pass that re-ranks and explains findings by
their real-world exposure. It is purely additive: it never suppresses a
finding and never changes the exit code (that is still computed from severity
alone), so it cannot mask a problem or gate CI. It is bring-your-own-key —
provide an API key via QK_LLM_API_KEY (or ANTHROPIC_API_KEY /
OPENAI_API_KEY); without a key, triage is skipped and the deterministic scan is
unaffected.
# Re-rank + annotate findings at/above the floor (default floor: medium).
qscan . --triage
# Only triage the most serious findings, with a specific provider/model.
qscan . --triage --triage-floor high --llm-provider anthropic --llm-model claude-sonnet-5
# See exactly what would be sent — contacts nothing, needs no key.
qscan . --triage --dry-run--context controls how much source leaves the machine (metadata | snippet |
function | file, default snippet); key material is redacted on every payload
(best-effort — pattern-based, so review with --dry-run). --dry-run prints the
exact, redacted payload and exits without contacting the provider, so you can
review what triage would send before enabling it.
Remediation (qremediate)
This package also ships a qremediate bin that applies fixes, not just
reports them. It scans, then for each finding a deterministic codemod proposes
a patch; every patch must clear a verify gate (the crypto finding is gone and
no new crypto finding is introduced) and a patch policy (only files that have
findings, plus dependency manifests) before it is offered.
qremediate . # print a unified diff of every verified fix (default; writes nothing)
qremediate . --mode apply # write the verified fixes into the working tree
qremediate . --mode pr # commit them to a new branch and open a DRAFT PR (never merges)Deterministic codemod fixes are safe to apply directly. With --llm (BYOK, same
keys as --triage), an LLM can also propose fixes codemods can't:
qremediate . --llm --mode diff # review LLM-proposed fixes as a diff first
qremediate . --llm --mode pr # or stage them as a draft PRReview LLM-proposed fixes before trusting them. The verify gate only confirms
the classical crypto finding was removed (a crypto-count check) — it is not a
full semantic safety review, so read every LLM-proposed change as a diff. Prefer
--mode diff or --mode pr for --llm fixes; --mode apply writes straight to
the working tree. Draft PRs are built in an isolated git worktree (your checkout is
never touched) and are never auto-merged.
CI
Fail the build on any high-or-worse finding, while tolerating an accepted baseline:
# .github/workflows/qscan.yml
name: qscan
on: [push, pull_request]
jobs:
quantum-readiness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Scan for quantum-vulnerable crypto
run: npx @quantakrypto/qscan . --severity-threshold high --baseline qscan-baseline.json
# Optional: upload SARIF to GitHub code scanning.
- name: Generate SARIF
if: always()
run: npx @quantakrypto/qscan . --format sarif -o qscan.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: qscan.sarifProgrammatic API
The CLI is a thin shell over runQscan, which @quantakrypto/action reuses:
import { runQscan, EXIT } from "@quantakrypto/qscan";
const { result, exitCode, suppressed } = await runQscan({
path: "src",
format: "json",
severityThreshold: "high",
baseline: "qscan-baseline.json",
});
console.log(`${result.findings.length} findings, exit ${exitCode}`);
if (exitCode === EXIT.FINDINGS) process.exitCode = 1;runQscan never touches process or stdout — it returns the rendered report
string and a suggested exitCode, leaving I/O to the caller. The package also
re-exports the argument parser (parseArgs, defaultOptions), severity helpers
(severityRank, meetsThreshold), and the canonical baseline utilities from
@quantakrypto/core (fingerprintFinding, baselineFromFindings, applyBaseline,
loadBaseline, saveBaseline) plus their legacy aliases (fingerprint,
buildBaseline, readBaseline, writeBaseline) for source compatibility.
Examples
See examples/ for a sample vulnerable project and the commands
to scan it.
License
Apache-2.0
Support & training
Questions, commercial support, or post-quantum readiness training for your team — visit quantakrypto.com or email [email protected].
