@quantakrypto/core
v0.12.0
Published
Shared post-quantum readiness library: crypto detectors, vulnerable-dependency database, inventory + SARIF reporting. Zero runtime dependencies.
Readme
@quantakrypto/core
Shared post-quantum readiness library for the quantakrypto toolchain. It finds classical, non-quantum-safe asymmetric cryptography in a codebase — inline crypto calls, embedded keys/certificates, and quantum-vulnerable npm dependencies — and turns the results into an inventory, a readiness score, and machine- or human-readable reports.
- Zero runtime dependencies. Node built-ins only.
- ESM + NodeNext, TypeScript strict, Node ≥ 20.
- Powers
@quantakrypto/qscan(CLI), the MCP server, and the GitHub Action.
Why it exists
Shor's algorithm breaks RSA, (EC)DH, ECDSA, DSA and EdDSA. Two threats follow:
- Harvest now, decrypt later (HNDL). Traffic protected by classical key
exchange / public-key encryption (ECDH, DH, RSA-OAEP) can be recorded today
and decrypted once a cryptographically relevant quantum computer exists.
Findings carry an
hndl: trueflag. - Forgery. Classical signatures (RSA-PSS, ECDSA, EdDSA, DSA, JWT
RS*/PS*/ES*/EdDSA) can be forged by a quantum attacker. These arehndl: falsebut still high severity.
@quantakrypto/core flags both, and points each finding at a NIST PQC replacement
(ML-KEM / FIPS 203, ML-DSA / FIPS 204, SLH-DSA / FIPS 205, hybrid
X25519MLKEM768).
Install
npm install @quantakrypto/coreQuick start
import { scan, formatSummary, toSarif, toJson } from "@quantakrypto/core";
const result = await scan({ root: "./", onFile: (f) => process.stderr.write(`scanning ${f}\n`) });
console.log(formatSummary(result, { color: true })); // human report
const sarif = toSarif(result); // SARIF 2.1.0 for CI
const json = toJson(result); // structured objectAPI
scan(options: ScanOptions): Promise<ScanResult>
Recursively scans a directory (or a single file) and returns findings, an inventory, file count, timing, and the tool version.
interface ScanOptions {
root: string; // directory or single file
include?: string[]; // restrict the walk to matching paths (substring/prefix)
exclude?: string[]; // extra exclude patterns (substring/prefix)
noDefaultIgnores?: boolean; // disable node_modules/.git/dist/… ignores
source?: boolean; // scan source files (default true)
dependencies?: boolean; // scan manifests/lockfiles across 7 ecosystems (default true)
config?: boolean; // scan PEM/TLS/cert config (default true)
maxFileSize?: number; // bytes; default 2 MiB (manifests are exempt)
scanMinified?: boolean; // scan minified/generated files (default false: skip them)
files?: string[]; // explicit relative file list (incremental scans)
detectors?: Detector[]; // override/extend the built-in detector set
onFile?: (file: string) => void; // progress callback (relative POSIX path)
}includeis now wired into the walker: when set, only paths matching one of the patterns are scanned.filesbypasses the directory walk entirely and scans the given relative paths (used for incremental / changed-files scans — pair it withchangedFiles). Binary and missing files are skipped; manifests over the size cap are still read.scanMinifiedis off by default; machine-minified / generated / bundled content (*.bundle.js,*.generated.ts, long single-line files, …) is skipped for speed unless you opt in.
scanParallel(options): Promise<ScanResult>
A worker-thread pool over the file list with a deterministic result merge
(byte-identical to scan). Falls back automatically to the in-process scan
for small workloads (below ~200 files / ~2 MiB) and whenever worker_threads
is unavailable. Extra options:
interface ParallelScanOptions extends ScanOptions {
concurrency?: number; // worker count; default os.availableParallelism()
parallelThresholdBytes?: number; // serial below this total size (default 2 MiB)
parallelFileThreshold?: number; // serial below this file count (default 200)
chunkBytes?: number; // target bytes per worker chunk (default 4 MiB)
}changedFiles(root, since?): Promise<string[]>
Returns relative POSIX paths that changed in a git work tree (uncommitted +
untracked, plus a since ref/range when provided). Tolerant of non-git
directories — returns []. Feed the result into ScanOptions.files for
incremental scans.
const files = await changedFiles(".", "origin/main...HEAD");
const result = await scan({ root: ".", files });walkFiles(root, options?): AsyncGenerator<string>
Recursive async generator yielding scannable text files as relative POSIX
paths. Skips default-ignored directories (node_modules, .git, dist,
build, .next, out, coverage, vendor, .turbo, .cache), honours
exclude patterns (substring or path-prefix), skips obvious binaries by
extension, and skips files larger than maxFileSize (default 2 MiB). root
may be a single file.
for await (const file of walkFiles("./src", { exclude: ["legacy"] })) {
console.log(file); // e.g. "components/Button.tsx"
}detectors: Detector[] / defaultRegistry / DetectorRegistry
The built-in, pure detectors. Each declares a scope ("source" | "config"),
a language, appliesTo(filePath), and detect({ file, content }). scan()
drives the source/config scope toggles from the detector's declared scope
(not from ruleId prefixes). Detector families:
| Detector | Scope | Catches |
| --- | --- | --- |
| node-crypto | source | generateKeyPair(Sync)('rsa'\|'ec'\|'dsa'\|'dh'\|'x25519'\|'x448'\|'ed25519'\|'ed448'), createSign/createVerify, one-shot crypto.sign/verify, createDiffieHellman, getDiffieHellman('modpN'), createECDH, publicEncrypt/privateDecrypt, diffieHellman |
| webcrypto | source | subtle.{generateKey,importKey,deriveKey,deriveBits,sign,verify,…} with RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDH, ECDSA |
| crypto-libs | source | node-forge (pki.rsa.generateKeyPair, ed25519), elliptic (new EC(...)), jsrsasign, node-rsa, direct secp256k1.* usage |
| jwt-jose | source | JWT/JOSE alg strings (RS/PS/ES*, EdDSA) and ECDH-ES* key agreement (HNDL) |
| tls-config | config | minVersion/secureProtocol: 'TLSv1'/'TLSv1.1', rejectUnauthorized: false, weak ciphers (RC4/DES/3DES/MD5/NULL/EXPORT) |
| pem-material | config | PEM keys/certs in any file: RSA/EC/DSA/PKCS#8/OPENSSH/PGP PRIVATE KEY, PGP MESSAGE, CERTIFICATE |
| ssh-cert | config | SSH public keys (ssh-rsa, ssh-ed25519, ecdsa-sha2-*) and X.509 certificate signature algorithms (sha256WithRSAEncryption, ecdsa-with-SHA256, …) |
| weak-hash-signature | config | SHA-1/MD5 in a digital-signature or X.509 certificate algorithm (SHA1withRSA, sha1WithRSAEncryption + OID, openssl -sha1 in a cert/sign command) |
| pqc-parameter | config | Post-quantum KEM parameter checks: pre-standard round-3 Kyber claimed as FIPS 203 ML-KEM (pqc-prestandard-kem), and an ML-KEM/Kyber byte size that names a different parameter set than the code advertises (pqc-parameter-mismatch) |
The rows above cover JavaScript/TypeScript plus the language-agnostic PEM/SSH/TLS surfaces. Thirteen further language packs apply the same RSA/EC/DSA/DH/Ed25519 detection to other ecosystems — including a smart-contract pack (Solidity/Move/ Cairo on-chain signature verification) — each a single umbrella detector:
| Detector | Scope | Language |
| --- | --- | --- |
| python-crypto | source | Python (cryptography, PyCryptodome, pyca) |
| go-crypto | source | Go (crypto/rsa, crypto/ecdsa, crypto/ecdh, crypto/ed25519, …) |
| java-crypto | source | Java/Kotlin (JCA KeyPairGenerator/Signature/KeyAgreement, BouncyCastle) |
| csharp-crypto | source | C#/.NET (RSA, ECDsa, ECDiffieHellman, DSA) |
| rust-crypto | source | Rust (rsa, p256/k256 ECDSA, x25519-dalek, ed25519-dalek) |
| ruby-crypto | source | Ruby (OpenSSL::PKey::{RSA,EC,DSA,DH}) |
| php-crypto | source | PHP (openssl_pkey_new, phpseclib RSA/EC/DSA) |
| elixir-crypto | source | Elixir/Erlang (:crypto, :public_key RSA/ECDSA/EdDSA) |
| c-crypto | source | C/C++ (OpenSSL EVP_PKEY_* / RSA_* / EC_* / DSA_* / DH_*) |
| swift-crypto | source | Swift (CryptoKit + Apple Security SecKey*) |
| objc-crypto | source | Objective-C (.m/.mm, Apple Security SecKey*) |
| dart-crypto | source | Dart/Flutter (pointycastle, cryptography — RSA/EC/Ed25519/X25519) |
defaultRegistry is a DetectorRegistry preloaded with these built-ins. The
registry is the plugin point:
import { DetectorRegistry, defaultRegistry, scan } from "@quantakrypto/core";
const registry = defaultRegistry.clone().register(myDetector);
const result = await scan({ root: ".", detectors: registry.all() });DetectorRegistry exposes register(d), get(id), has(id), all() and
clone(). Ids must be unique (duplicate registration throws).
Public plugin surface vs. internal helpers. The stable, frozen way to add a detector from outside this package is the
Detectorinterface plus thescan({ detectors })/DetectorRegistryhook shown above — build your rule metadata with the publicRuleMetatype and returnFinding[]. The lexical conveniences the in-repo detectors use (findingFromRule,eachMatch, the comment maskers,hasExtension, …) are internal and intentionally not part of the frozen API: they churn with the scanner internals. The step-by-step guide below is for in-repo contributors (it editsregistry.ts,detect-utils.ts,comments.ts), who can import those helpers directly.
Adding a detector / language (in-repo contributors)
- Create
src/detectors/<lang>.tsexporting one or moreDetectors. Setlanguage(a member ofDetectorLanguageintypes.ts— add a new one for a new language pack),scope("source" | "config"), anappliesTo(path)extension check, and a puredetect({ file, content })returningFinding[]. Build findings withfindingFromRule(ruleMeta, at, overrides?)(notmakeFinding) so every rule's metadata lives once in itsRuleMetacatalog entry. - Define
<LANG>_EXTENSIONSindetect-utils.tsnext to the other packs (do not inline the list in the detector) and gateappliesToon it. - Wire the new SOURCE language into coverage + comment handling (skipping this is
how a pack half-lands): add the extensions to
ANALYZABLE_SOURCE_EXTENSIONSand the name toANALYZABLE_LANGUAGES_LABEL(detect-utils.ts) soanalyzedFiles/coverage count it; and add them to the comment table incomments.ts(C_LIKEfor//+/* */,HASH_LIKEfor#) so commented-out code is suppressed. Aconfigdetector instead masks its own comment lines withmaskCommentLines/maskBlockComments. - Register it in
builtinDetectors(registry.ts) — the single source of truth the default registry and publicdetectorsexport are built from. (Ad-hoc: pass{ detectors: [...] }toscan.) No edit toscan()is needed; scope is honoured from the detector's declaredscope. - Add tests: at least one positive per rule, one negative/FP-bait, and a gating test;
consider a labelled benchmark corpus fixture (
test/benchmark/). For a new dependency ecosystem, also extendVulnerableDependency.ecosystem+ a matcher independencies.ts.
vulnerableDependencies: VulnerableDependency[]
Curated database (90 entries) of packages whose purpose is classical
asymmetric crypto, spanning eight ecosystems — npm, PyPI, Cargo, Go modules,
Maven, RubyGems, and NuGet (VulnerableDependency.ecosystem). The npm subset includes
node-forge, elliptic, jsrsasign, node-rsa, ursa, sshpk,
jsonwebtoken, jose, jws, eccrypto, secp256k1, tweetnacl, ed25519,
@noble/curves, @noble/secp256k1, @noble/ed25519, paseto, bcrypto,
ecpair, keypair. scan() matches these against each ecosystem's manifests
and lockfiles — package.json / package-lock.json / yarn.lock /
pnpm-lock.yaml, requirements.txt / pyproject.toml / Pipfile, Cargo.toml,
go.mod, pom.xml / build.gradle, Gemfile / *.gemspec, *.csproj /
packages.config / directory.packages.props — and emits
category: "dependency" findings located at the manifest.
scanAdvisories(root, opts?) / checkProvenance(root, opts?) (opt-in, qscan --audit)
Two supply-chain helpers layered on top of the crypto scan (opt-in because they shell out or make a network request):
scanAdvisories(root, opts?): Promise<{ findings; diagnostics }>— shells out (execFile, bounded timeout + buffer, in a try/catch — the blessedchanged.tspattern) to each present ecosystem's own audit tool (cargo audit --json,pip-audit --format json,npm audit --json) and turns its advisories intodep-advisory(category: "dependency") findings. A missing tool (ENOENT) or any error degrades to a diagnostic string, never throws.DEP_ADVISORY_RULEis the generic SARIF catalog entry.checkProvenance(root, opts?): Promise<{ findings; diagnostics }>— reads the root manifest's declared repository (package.json/Cargo.toml/pyproject.toml). No repository →provenance-repo-missing(info). Withopts.networkand an injectedheadrequester, a declared URL that 404s or does not resolve →provenance-repo-unresolved(medium). Core stays offline (ADR-0005): the actualnode:httpsHEAD request is injected by the caller (qScan).PROVENANCE_RULESare the generic SARIF catalog entries.
buildInventory(findings: Finding[]): CryptoInventory
Aggregates findings into per-algorithm / per-category / per-severity counts, the
HNDL count, and a readinessScore (0–100, 100 = no classical asymmetric crypto
found). The score starts at 100 and subtracts severity-weighted penalties with
diminishing returns per severity bucket, clamped to [0, 100].
Reporters
toSarif(result): SarifLog— valid SARIF 2.1.0 ($schema,version,runs[0].tool.driver { name: "qScan", informationUri, version, rules[] },results[]withruleId,level(error/warning/note),message.text,locations[].physicalLocationwithartifactLocation.uriandregion.startLine/startColumn).toJson(result): Record<string, unknown>— clean, JSON-serialisable object.formatSummary(result, { color? }): string— human report with the readiness score, severity/algorithm breakdown, top findings, and an HNDL note. Colour is off by default and uses raw ANSI codes when enabled.
remediationFor(algorithm): Remediation | undefined / remediationForTier(algorithm, tier?)
remediationFor returns the recommended PQC replacement for a classical family.
remediationForTier adds a security tier: "category-3" (default,
commercial — ML-KEM-768 / ML-DSA-65) or "category-5" (CNSA 2.0 / long-lived —
ML-KEM-1024 / ML-DSA-87).
remediationFor("ECDH");
// { algorithm: "ECDH", recommendation: "hybrid X25519MLKEM768 (ML-KEM-768)", detail: … }
remediationForTier("ECDH", "category-5");
// recommendation mentions ML-KEM-1024; detail cites CNSA 2.0 (2030/2033 milestones)STATEFUL_HBS_NOTE / statefulHbsApplies(algorithm) surface the SP 800-208
stateful hash-based signatures (LMS / XMSS / HSS) guidance for firmware / boot
signing (stateful — use only with rigorous state management).
Baseline (fingerprintFinding, baselineFromFindings, applyBaseline, loadBaseline, saveBaseline)
The single canonical baseline scheme shared by qScan and the Action. A baseline
is { version, fingerprints: string[] }. A fingerprint is the SHA-256 hex of
ruleId|file|normalizedSnippet — line-insensitive (survives line shifts)
and snippet-whitespace-normalized (survives reformatting).
import { saveBaseline, loadBaseline, applyBaseline } from "@quantakrypto/core";
await saveBaseline(".quantakrypto-baseline.json", result.findings); // write
const baseline = await loadBaseline(".quantakrypto-baseline.json"); // read (tolerant)
const { newFindings, suppressed } = applyBaseline(result.findings, baseline);toCbom(result): CycloneDxBom
A CycloneDX 1.6 cryptographic bill of materials (CBOM): one
cryptographic-asset component per distinct (assetType, algorithm, discriminator),
with occurrence evidence and quantumVulnerable / harvestNowDecryptLater flags.
Each finding is classified into its proper CycloneDX assetType — algorithm
(crypto usage), certificate (X.509), related-crypto-material (private/public
key material), or protocol (TLS). Deterministic output.
toOpenVex(result, opts?): OpenVexDocument
An OpenVEX 0.2.0 document: one statement per rule (a synthetic QK-<ruleId>
vulnerability, since PQC findings have no CVE), every affected file:line product,
status: "affected", the rule's remediation as action_statement, and any
--triage verdict in status_notes. Deterministic output; feeds VEX pipelines.
Evidence chain — buildReadinessReport / signReadinessReport / verifyReadinessReport
The ISO/IEC 27001 A.8.24 readiness report (see docs/compliance/iso27001-a8.24-evidence.md):
buildReadinessReport(result, opts?)— bundles the scan result, inventory, CBOM, and a deterministicattestation.contentHash(excludes the volatile scan time).signReadinessReport(report, { signer?, timestamper? })— orchestrates an EXTERNAL signer/timestamper over thecontentHash(ADR-0004; the tool implements no crypto).verifyReadinessReport(report)— recomputes the content hash to detect tampering with any attested field, returning{ valid, computedHash, claimedHash, reason? }.
CWE tagging
Every detector sets a Finding.cwe (CWE-327 broken crypto, CWE-326 weak
strength, CWE-295 cert validation, CWE-798 hardcoded key). toSarif maps it
into rules[].properties, result taxa, and a run-level CWE taxonomies
component; toJson includes it. Constants are exported (CWE_BROKEN_CRYPTO, …).
VERSION: string
Tool version surfaced in reports (kept in sync with package.json).
API reference
| Export | Kind | Summary |
| --- | --- | --- |
| scan | fn | Scan a dir / file / explicit file list → ScanResult |
| scanParallel | fn | Worker-pool scan with deterministic merge + serial fallback |
| changedFiles | fn | Git-aware changed-files list for incremental scans |
| detectFile | fn | Pure per-file detect (used by workers / tests) |
| compareFindings | fn | Stable finding comparator (file → line → ruleId) |
| walkFiles, isBinaryPath, looksMinified | fn | Walker + file-classification helpers |
| detectors | const | Built-in detector array (mirrors defaultRegistry.all()) |
| DetectorRegistry, defaultRegistry | class/const | Detector plugin point |
| buildInventory | fn | Aggregate findings → CryptoInventory |
| vulnerableDependencies | const | Curated quantum-vulnerable dependency DB (7 ecosystems) |
| toSarif, toJson, formatSummary | fn | Reporters (SARIF 2.1.0 / JSON / human) |
| toCbom | fn | CycloneDX 1.6 CBOM export (assetType-classified) |
| toOpenVex | fn | OpenVEX 0.2.0 export |
| buildReadinessReport, signReadinessReport, verifyReadinessReport | fn | ISO A.8.24 evidence chain (build / sign / verify) |
| remediationFor, remediationForTier, TIER_PARAMS | fn/const | PQC remediation (family + CNSA tier) |
| STATEFUL_HBS_NOTE, statefulHbsApplies | const/fn | SP 800-208 LMS/XMSS guidance |
| fingerprintFinding, baselineFromFindings, applyBaseline, loadBaseline, saveBaseline, BASELINE_VERSION | fn/const | Canonical baseline |
| verifyFix, languageToExtension | fn | Snippet-level fix verification (crypto finding removed?) + language→extension map |
| buildContext, renderPreflight | fn | Redacted source-context builder + preflight preview (secrets stripped) |
| TRIAGE_RUBRIC, TRIAGE_VERDICT_SCHEMA, buildTriageRequest | const/fn | Deterministic, offline triage-request bundle (rubric + verdict schema) |
| REMEDIATE_RUBRIC, FIX_REQUEST_SCHEMA, buildRemediateRequest | const/fn | Deterministic, offline remediation-request bundle (rubric + fix schema) |
| checkPatchPolicy | fn | Patch-policy gate — only files with findings + dependency manifests |
| withWorktree | fn | Run a callback inside an ephemeral git worktree (isolates writes; no auto-merge) |
| codemodRegistry, codemodFor, configToggleCodemod | const/fn | Deterministic codemod registry + per-finding lookup |
| remediateFindings | fn | Remediation pipeline: propose → policy + verify gate → verified patches |
| loadConfig, ConfigError, CONFIG_FILENAME | fn/const | quantakrypto.config.json loader |
| AbortError, BudgetExceededError | class | Scan cancellation / work-budget overflow errors |
| CWE_BROKEN_CRYPTO, CWE_WEAK_STRENGTH, CWE_CERT_VALIDATION, CWE_HARDCODED_KEY, CWE_RISKY_PRIMITIVE | const | CWE identifiers |
| VERSION | const | Tool version |
Types: Finding (now with optional cwe), ScanOptions/ParallelScanOptions,
ScanResult, Detector (now with scope/language), DetectorScope,
DetectorLanguage, Baseline, CycloneDxBom, CbomComponent, SecurityTier,
AlgorithmFamily (now includes X448), and the rest of the locked contract in
src/types.ts.
Core types
See src/types.ts for the locked contract. Highlights:
type Severity = "critical" | "high" | "medium" | "low" | "info";
type AlgorithmFamily =
| "RSA" | "ECDH" | "ECDSA" | "EdDSA"
| "DH" | "DSA" | "X25519" | "X448" | "ECIES" | "unknown";
type FindingCategory =
| "kem" | "key-exchange" | "signature"
| "tls" | "certificate" | "dependency" | "hash" | "rng";
interface Finding {
ruleId: string;
title: string;
category: FindingCategory;
severity: Severity;
confidence: "high" | "medium" | "low";
algorithm?: AlgorithmFamily;
hndl: boolean; // harvest-now-decrypt-later exposure
message: string;
remediation?: string;
cwe?: string; // e.g. "CWE-327"
location: { file: string; line: number; column?: number; endLine?: number; snippet?: string };
}
interface CryptoInventory {
byAlgorithm: Partial<Record<AlgorithmFamily, number>>;
byCategory: Partial<Record<FindingCategory, number>>;
bySeverity: Record<Severity, number>;
hndlCount: number;
readinessScore: number; // 0–100
}Example
A runnable example lives in examples/scan-example.mjs:
node examples/scan-example.mjs ./path/to/projectDevelopment
npm run build # tsc -b
npm test # node --import tsx --test test/*.test.tsTests use only node:test + node:assert. The package has zero runtime
dependencies.
License
Apache-2.0
Support & training
Questions, commercial support, or post-quantum readiness training for your team — visit quantakrypto.com or email [email protected].
