Snyk for Post-Quantum Cryptography — a developer-first static analysis tool that finds quantum-vulnerable cryptography in your code, configs, dependencies, keys and certificates before quantum computers (or your auditors) do.
pqc-scan is zero-friction and lives where your code lives: in your editor, on the
command line, and right inside your pull requests. It parses real source with
tree-sitter ASTs (not brittle regexes), tells you exactly which line is vulnerable,
why it matters, and how to migrate to a NIST-standardized post-quantum algorithm.
pqc-scan scan . pqc-scan · Post-Quantum Cryptography scan
✖ CRITICAL PQC001 · RSA Key Generation [high confidence]
┌─ app/keys.py:10:19 (RSA-2048)
│ rsa.generate_private_key(
│ public_exponent=65537,
│ key_size=2048,
│ )
└─ RSA key generation detected. RSA is broken by Shor's algorithm on a
cryptographically relevant quantum computer, regardless of key size.
Migrate to: ML-KEM-768 (CRYSTALS-Kyber) … · FIPS 203 (ML-KEM) / FIPS 204 (ML-DSA)
see: https://csrc.nist.gov/pubs/fips/203/final
────────────────────────────────────────────────────────────────────────────────
Critical: 2 High: 2 Medium: 0 Low: 0 | Total findings: 4
Files scanned: 1 | Time: 0.00s
The public-key cryptography that secures almost everything online — TLS handshakes, SSH, code signing, JWTs, certificates, VPNs — relies on math problems that are hard for classical computers. Shor's algorithm, run on a cryptographically relevant quantum computer, solves all of them efficiently. That breaks, completely:
- RSA (any key size, including RSA-4096)
- Elliptic-curve crypto: ECDSA, ECDH/ECDHE, X25519/X448, Ed25519/Ed448
- Finite-field crypto: Diffie-Hellman (DH/DHE), DSA
Symmetric crypto and hashing are weakened but not broken: Grover's algorithm only halves the effective security level, so AES-256 and SHA-256/SHA3-256 remain safe.
You don't need a quantum computer to exist today to be at risk. Adversaries can capture encrypted traffic now and decrypt it later once quantum hardware matures. Any data with a long confidentiality lifetime — health records, financial data, state secrets, long-lived credentials — is already exposed. Key-exchange material (ECDH, X25519, DH) is the prime target.
- August 2024 — NIST finalized the first post-quantum standards: FIPS 203 (ML-KEM) for key encapsulation, FIPS 204 (ML-DSA) and FIPS 205 (SLH-DSA) for digital signatures.
- 2030 / 2035 — US federal guidance (CNSA 2.0, NSM-10) sets a migration deadline: begin now, complete the transition for most systems by 2030, finish by 2035.
Migration is a multi-year inventory-and-replace effort. pqc-scan is the inventory
step you can run on every commit.
pqc-scan requires Python ≥ 3.10.
Install editable from a clone of the source:
git clone https://github.com/pqc-scan/pqc-scan.git
cd pqc-scan
pip install -e .This installs the pqc-scan console script. Verify:
pqc-scan --version
# pqc-scan 0.2.1For development (tests + coverage):
pip install -e ".[dev]"
pytestAll scanning dependencies (tree-sitter grammars for Python, JavaScript, Java, Go and Rust, plus Typer, Rich and PyYAML) are installed automatically — there is no native toolchain to build. Key and certificate parsing uses a small built-in DER reader, so no OpenSSL or Rust toolchain is required either.
pqc-scan scan . # scan the current tree, pretty console report
pqc-scan scan src/ -s high # only report HIGH and CRITICAL findings
pqc-scan scan . -o sarif -f out.sarif # write SARIF for GitHub code scanning
pqc-scan scan . -o markdown # a report you can paste into a PR comment
pqc-scan scan . --changed-only # only files changed in the current git diff
pqc-scan scan . -s medium --fail-on critical # report widely, block narrowly
pqc-scan baseline . # accept today's findings; gate on new ones
pqc-scan explain PQC001 # full rule + before/after migration example
pqc-scan rules # list every detection rule
pqc-scan init # write a starter .pqcscan.ymlpqc-scan exposes six commands: scan, report, baseline, init, rules,
and explain.
pqc-scan scan [PATH] [OPTIONS]
PATH defaults to . (the current directory) and may be a file or a directory.
| Flag | Alias | Description |
|---|---|---|
--output |
-o |
Output format: console (default), sarif, cbom, json, markdown. |
--output-file |
-f |
Write output to this file instead of stdout. |
--severity |
-s |
Minimum severity to report: critical, high, medium, low. |
--exclude |
Glob pattern to exclude. Repeatable. | |
--changed-only |
Only scan files changed in the current git diff (fast PR scans). | |
--config |
Path to a .pqcscan.yml config file. |
|
--no-color |
Disable colored output (auto-disabled when writing to a file). | |
--fail-on-findings |
Exit with code 1 if any findings are reported — for CI gating. |
|
--fail-on SEV |
Exit with code 1 only when a finding is at least this severe. |
|
--baseline FILE |
Report only findings that are not in this baseline. | |
--no-baseline |
Ignore a baseline configured in .pqcscan.yml. |
|
--no-suppress |
Ignore inline pqc-scan: ignore directives (audit mode). |
|
--show-suppressed |
Also list suppressed findings, with their reasons. | |
--limit N |
Show at most N findings in console output (0 = all). |
|
--summary |
Console output: totals and a per-file breakdown only, no per-finding detail. | |
--group-by |
Console grouping: severity (default) or file. |
Examples:
# Console report, but gate CI: non-zero exit if anything is found.
pqc-scan scan . --fail-on-findings
# Scan only application code, skip tests and vendored code.
pqc-scan scan . --exclude "**/tests/**" --exclude "**/third_party/**"
# Emit machine-readable JSON to stdout (pipe into jq, dashboards, etc.).
pqc-scan scan src/ -o json
# Only audit what this branch changed, at HIGH severity and above.
pqc-scan scan . --changed-only -s high --fail-on-findings
# Report everything from medium up, but only fail the build on a critical.
pqc-scan scan . -s medium --fail-on critical
# Large repo? Get the overview first, then drill into one file at a time.
pqc-scan scan . --summary
pqc-scan scan . --group-by file --limit 50Example console output (scanning a single file):
pqc-scan · Post-Quantum Cryptography scan
✖ CRITICAL PQC001 · RSA Key Generation [high confidence]
┌─ app/keys.py:10:19 (RSA-2048)
│ rsa.generate_private_key(
│ public_exponent=65537,
│ key_size=2048,
│ )
└─ RSA key generation detected. RSA is broken by Shor's algorithm on a
cryptographically relevant quantum computer, regardless of key size.
Migrate to: ML-KEM-768 (CRYSTALS-Kyber) for encryption / key
establishment, or ML-DSA-65 … · FIPS 203 (ML-KEM) / FIPS 204 (ML-DSA)
see: https://csrc.nist.gov/pubs/fips/203/final
⚠ HIGH PQC002 · RSA Encryption / Padding [high confidence]
┌─ app/keys.py:26:9 (RSA-OAEP)
│ padding.OAEP(
│ mgf=padding.MGF1(algorithm=hashes.SHA256()),
│ algorithm=hashes.SHA256(),
│ label=None,
│ )
└─ RSA-based encryption or padding (OAEP / PKCS1v15) detected. RSA encryption
is broken by Shor's algorithm.
Migrate to: ML-KEM-768 (CRYSTALS-Kyber) · FIPS 203
see: https://csrc.nist.gov/pubs/fips/203/final
────────────────────────────────────────────────────────────────────────────────
Critical: 2 High: 2 Medium: 0 Low: 0 | Total findings: 4
Files scanned: 1 | Time: 0.00s
report runs a scan and writes a machine-readable artifact to disk. Unlike scan,
the format defaults to cbom and --output-file is required.
pqc-scan report [PATH] --output-file FILE [--format cbom|sarif|json|markdown] [--config FILE]
# Generate a CycloneDX Cryptography Bill of Materials.
pqc-scan report . --output-file cbom.json
# Generate a SARIF report for archival / upload.
pqc-scan report . --format sarif --output-file results.sarifpqc-scan baseline [PATH] [--output-file FILE] [--severity SEV] [--exclude GLOB] [--config FILE]
Writes .pqcscan-baseline.json (by default, next to PATH). Commit it, then
point scan at it so only new findings are reported. See
Baselines below.
pqc-scan explain PQC001 [--json]
Prints the rule, its severity and category, the recommended post-quantum replacement, and the before/after code example — which is the one piece of migration guidance that console and SARIF output are too terse to show.
PQC001 RSA Key Generation critical (key-generation · pke)
RSA key generation detected. RSA is broken by Shor's algorithm on a
cryptographically relevant quantum computer, regardless of key size.
Migrate to
ML-KEM-768 (CRYSTALS-Kyber) for encryption / key establishment, or
ML-DSA-65 (CRYSTALS-Dilithium) if the key is used for signing
FIPS 203 (ML-KEM) / FIPS 204 (ML-DSA)
...
pqc-scan init [PATH] [--force]
Writes a starter .pqcscan.yml to PATH (default .). Refuses to overwrite an
existing file unless --force is given.
pqc-scan init
# Created .pqcscan.ymlpqc-scan rules # table
pqc-scan rules --json # the registry, for your own toolingPrints every rule (ID, name, severity, category, description).
pqc-scan ships 16 rules, covering quantum-vulnerable key generation, signatures,
encryption, key exchange, hashing, weak JWT/TLS configuration, legacy ciphers,
quantum-vulnerable dependencies, and deployed key material and certificates.
| ID | Name | Severity | Category |
|---|---|---|---|
| PQC001 | RSA Key Generation | critical |
key-generation |
| PQC002 | RSA Encryption / Padding | high |
encryption |
| PQC003 | RSA Signature | critical |
signing |
| PQC004 | ECDSA Key Generation or Signing | critical |
signing |
| PQC005 | ECDH / X25519 Key Exchange | high |
key-exchange |
| PQC006 | Ed25519 / Ed448 Key Generation | high |
signing |
| PQC007 | Diffie-Hellman Key Exchange | high |
key-exchange |
| PQC008 | DSA Key Generation or Signing | critical |
signing |
| PQC009 | SHA-1 Usage | medium |
hashing |
| PQC010 | MD5 Usage | high |
hashing |
| PQC011 | Weak JWT Algorithm (RS/ES/PS) | high |
signing |
| PQC012 | Weak TLS Configuration | medium |
configuration |
| PQC013 | DES / 3DES Usage | high |
encryption |
| PQC014 | Quantum-Vulnerable Dependency | medium |
dependency |
| PQC015 | Quantum-Vulnerable Key Material | high |
key-material |
| PQC016 | Quantum-Vulnerable Certificate | high |
certificate |
Run pqc-scan rules for the full descriptions and to confirm the set installed on
your machine.
Supported languages (code): Python, JavaScript/TypeScript, Java, Go, Rust.
Supported dependency manifests: requirements*.txt, setup.py,
pyproject.toml, Pipfile, package.json, Cargo.toml, go.mod, pom.xml,
build.gradle / build.gradle.kts, Gemfile / *.gemspec, composer.json.
Each is parsed in its own format — a version string or repository URL that merely
contains a library name does not fire. Lock files are deliberately out of scope:
they restate the manifest plus the whole transitive closure and would bury the
actionable direct declaration.
Supported configs: YAML / JSON / TOML / .conf / .ini / .env (TLS, JWT,
cipher lists, protocol versions).
Supported key material: .pem, .crt, .cer, .key, .pub, .csr,
id_*, authorized_keys, known_hosts — and PEM blocks inlined into
configuration files.
Library coverage highlights (beyond the language standard libraries):
- Python —
cryptography(hazmat), pycryptodome/pycrypto (RSA.generate,pkcs1_15,pss,DSS,PKCS1_OAEP), pyOpenSSL (TYPE_RSA), paramiko, PyJWT / python-jose,ssl,hashlib(includinghashlib.new("sha1")and theusedforsecurity=Falsedemotion tolow). - JavaScript — Node
crypto(generateKeyPair,createDiffieHellman,publicEncrypt,createSign), WebCryptoSubtleCrypto,jsonwebtoken,jose(setProtectedHeader), node-forge, AWS KMS asymmetricKeySpecs. - Java — JCA factories (
KeyPairGenerator,Signature,Cipher,MessageDigest,KeyAgreement), Bouncy Castle lightweight API (RSAKeyGenerationParameters,ECDSASigner,Ed25519Signer, …), andSSLContext.getInstancewith legacy protocols. - Go —
crypto/rsa,crypto/ecdsa,crypto/ecdh,crypto/ed25519,crypto/dsa,crypto/tlsconfiguration (MinVersionpins and weakCipherSuites),x/crypto/curve25519, and golang-jwt signing methods. - Rust — RustCrypto (
rsa,p256/p384/p521/k256,ed25519-dalek,x25519-dalek,sha1,md-5,des),ring(signature::*,agreement::*,digest::SHA1_*), theopensslbindings (Rsa::generate,EcKey,Dsa,Dh,MessageDigest::sha1, DESCiphers, legacySslVersion), andjsonwebtoken.
Context hints. Findings inside code that looks like crypto-library
plumbing (paths containing hazmat, _internal, backends, vendor, …) or
inside a generate_*_key()-style wrapper carry a context_hint explaining
whether the call site is actionable for you or belongs to a library you merely
consume. Hints appear in console output, SARIF properties.contextHint, and
the JSON context_hint field.
Keys and certificates. PQC015 / PQC016 read the actual DER, not the PEM
label, so a finding names the real parameters — RSA-3072, ECDSA-P-384,
Ed25519 — along with a certificate's subject CN, signature algorithm and
expiry:
⚠ HIGH PQC016 · Quantum-Vulnerable Certificate [high confidence]
┌─ deploy/tls/server.crt:1:1 (RSA-2048)
│ -----BEGIN CERTIFICATE-----
└─ X.509 certificate with a RSA-2048 public key (subject CN=api.example.com;
signed with sha256WithRSAEncryption; expires 2027-03-04). …
That matters for precision as much as for detail: a PKCS#8 block labelled
PRIVATE KEY may hold RSA, EC, Ed25519 — or ML-DSA, which must not be
flagged. Only algorithms on a known quantum-vulnerable OID list produce a
finding, so real ML-KEM / ML-DSA keys and ML-DSA-signed certificates come back
clean. A certificate signed with SHA-1 or MD5 is additionally reported as
critical, because that signature is forgeable today without any quantum
computer.
Two features exist so that dropping pqc-scan into a repository that already has
crypto does not mean a permanently red build or a disabled scanner.
Waive a finding next to the code that needs it, with a reason:
key = rsa.generate_private_key(...) # pqc-scan: ignore[PQC001] -- legacy peer, JIRA-42
# pqc-scan: ignore-next-line[PQC004,PQC005]
key = ec.generate_private_key(ec.SECP256R1())// pqc-scan: ignore-file[PQC014] -- vendored reference implementation| Directive | Applies to |
|---|---|
pqc-scan: ignore |
the line it appears on |
pqc-scan: ignore-next-line |
the following line |
pqc-scan: ignore-file |
every finding in the file |
- A bracketed list scopes the waiver (
ignore[PQC001,PQC009]); no brackets means every rule. Rule IDs are case-insensitive. - Text after
--is recorded as the reason and reported back. - The parser is comment-syntax agnostic —
#,//,/* */,<!-- -->and YAML comments all work, in source, configs and manifests alike. The trade-off is that a directive inside a string literal also suppresses.
Waivers stay auditable rather than disappearing:
- suppressed findings never gate CI and are not counted in
total; - they are emitted in SARIF with
suppressions: [{ "kind": "inSource" }], which GitHub code scanning honors — the finding is recorded, not shown as an open alert; - they appear in JSON under
suppressed_findings, and in the console summary as(+N suppressed);--show-suppressedlists them with their reasons; - they are still inventoried in the CBOM, because an accepted risk is still a deployed algorithm.
--no-suppress (or suppressions: false in config) ignores every directive, for
an audit run that must see everything.
A baseline says "this is the debt we already have; tell me about anything new."
# 1. Snapshot the current findings and commit the file.
pqc-scan baseline .
git add .pqcscan-baseline.json
# 2. In CI, gate on new crypto only.
pqc-scan scan . --baseline .pqcscan-baseline.json --fail-on highOr configure it once, in .pqcscan.yml:
baseline: .pqcscan-baseline.jsonFingerprints hash the rule ID, the baseline-relative file path, the algorithm and
the whitespace-normalized snippet — not the line number. Adding an import
above a finding does not invalidate the baseline, and neither does reformatting.
Identical findings are tracked by count, so if a file baselines two
hashlib.sha1() calls and someone adds a third, the third is reported as new.
The document keeps rule_id / path / algorithm alongside each hash, so a
baseline diff is reviewable in a pull request instead of being an opaque wall of
hashes. Regenerating prints what changed since the previous snapshot.
Baselined findings are kept out of SARIF — the whole point is that pre-existing
debt stops annotating pull requests — but remain in the CBOM inventory and in the
JSON baselined_findings array. A missing or malformed baseline file exits 2
rather than silently treating everything as new.
Choose with -o/--output on scan, or --format on report.
A colorized, human-readable report with severity badges, the exact code snippet, the
migration target, and the relevant NIST standard. Color is auto-disabled when writing
to a file or with --no-color.
Static Analysis Results Interchange Format 2.1.0. Upload it with the CodeQL action and findings appear as inline annotations on the exact line in pull requests and in the repository's Security → Code scanning tab.
pqc-scan scan . -o sarif -f pqc-scan.sarif{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/.../sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": { "driver": { "name": "pqc-scan", "version": "0.2.1", "rules": [ ... ] } },
"results": [ ... ]
}
]
}A CycloneDX 1.6 CBOM: a structured inventory of every cryptographic asset found,
emitted as cryptographic-asset components. Ideal for compliance, supply-chain
attestation, and tracking migration progress over time.
pqc-scan report . --format cbom --output-file cbom.json{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"components": [
{ "type": "cryptographic-asset", "name": "RSA-2048", "...": "..." }
]
}A report you can paste into a PR comment or pipe straight into
$GITHUB_STEP_SUMMARY: a counts table, one scannable row per finding, and the
migration guidance folded into <details> blocks so the comment stays short
until someone opens one.
pqc-scan scan . -o markdown -f pqc-scan.md## 🔐 pqc-scan — Post-Quantum Cryptography scan
| Critical | High | Medium | Low | Total |
| ---: | ---: | ---: | ---: | ---: |
| 2 | 2 | 0 | 0 | **4** |
_1 file(s) scanned · 0.01s_
### Findings
| Severity | Rule | Name | Location | Algorithm |
| --- | --- | --- | --- | --- |
| 🔴 critical | `PQC001` | RSA Key Generation | `app/keys.py:10` | RSA-2048 |Table rows and detail blocks are capped independently (100 and 20 by default) so the output stays under GitHub's 65,536-character comment limit; whatever is left out is stated explicitly rather than silently truncated. The GitHub Action writes this to the job summary page automatically.
Scan metadata plus every finding with full migration metadata — easy to pipe into
jq, dashboards, or custom tooling.
pqc-scan scan . -o json | jq '.findings[] | {rule_id, severity, file_path, line_number, algorithm}'{
"tool": "pqc-scan",
"version": "0.2.1",
"generated_at": "2026-07-06T12:00:00Z",
"paths": ["/repo"],
"summary": {
"total": 4,
"by_severity": { "critical": 2, "high": 2, "medium": 0, "low": 0 },
"files_scanned": 128,
"duration_seconds": 0.4211,
"errors": []
},
"findings": [
{
"file_path": "app/keys.py",
"line_number": 10,
"column_number": 19,
"algorithm": "RSA-2048",
"category": "key-generation",
"severity": "critical",
"confidence": "high",
"rule_id": "PQC001",
"context_hint": null,
"migration_suggestion": {
"recommended_algorithm": "ML-KEM-768 (CRYSTALS-Kyber) …",
"nist_standard": "FIPS 203 (ML-KEM) / FIPS 204 (ML-DSA)",
"docs_url": "https://csrc.nist.gov/pubs/fips/203/final"
}
}
]
}Run pqc-scan on every pull request and surface findings as inline code-scanning
annotations. Save this as .github/workflows/pqc-scan.yml:
name: pqc-scan
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
security-events: write # required to upload SARIF to code scanning
jobs:
pqc-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # only needed for changed-only
- name: Run pqc-scan
uses: ./ # this repo's action; or pin: pqc-scan/pqc-scan@v1
with:
path: .
severity: medium # report from medium up...
fail-on: critical # ...but only block the PR on a critical
changed-only: ${{ github.event_name == 'pull_request' }}The action uploads the SARIF to code scanning itself, so no separate
upload-sarif step is required.
Action inputs
| Input | Default | Description |
|---|---|---|
path |
. |
File or directory to scan. |
severity |
medium |
Minimum severity to report. |
fail-on |
'' |
Fail the job only when a finding is at least this severe. |
fail-on-findings |
false |
Fail the job if any finding is reported. |
changed-only |
false |
Only scan files changed in this PR (needs fetch-depth: 0). |
baseline |
'' |
Baseline file of accepted findings; report only what is new. |
config |
'' |
Path to a .pqcscan.yml config file. |
output-sarif |
pqc-scan.sarif |
Path to write the SARIF report. |
upload-sarif |
true |
Upload the SARIF to GitHub code scanning. |
job-summary |
true |
Write the Markdown report to the job summary page. |
python-version |
3.12 |
Python used to run pqc-scan. |
Action outputs
| Output | Description |
|---|---|
sarif-file |
Path of the SARIF report that was written. |
total-findings |
Number of findings reported by the scan. |
severity and fail-on are deliberately separate: the common setup reports
everything from medium up — so it all shows as code-scanning annotations and in
the job summary — while only a critical finding actually blocks the pull
request. The SARIF is written before the failing exit, so annotations appear even
on a red build.
If you prefer to run the CLI directly without the composite action, the equivalent step is just:
- run: pip install -e . && pqc-scan scan . -o sarif -f pqc-scan.sarif -s highPair it with --changed-only in PR jobs to scan only the diff and keep runs fast.
pqc-scan reads a .pqcscan.yml file. It is discovered automatically by walking up
from the scanned path, or pointed at explicitly with --config. Generate a starter
with pqc-scan init. Full schema:
# .pqcscan.yml — configuration for pqc-scan
exclude:
- "**/tests/**"
- "**/*.test.py"
- "**/vendor/**"
- "**/node_modules/**"
# Minimum severity to report: critical | high | medium | low
severity_threshold: medium
languages:
- python
- javascript
- java
- go
- rust
scan_configs: true # Scan YAML/JSON/TOML/.conf config files
scan_dependencies: true # Scan dependency manifests (requirements.txt, Cargo.toml, ...)
scan_certificates: true # Scan PEM/OpenSSH keys and X.509 certificates (PQC015/PQC016)
# Honor inline "pqc-scan: ignore" directives (set false for an audit run).
suppressions: true
# Path to a baseline of accepted findings, relative to this file. Generate it
# with `pqc-scan baseline`; later scans then report only NEW findings.
# baseline: .pqcscan-baseline.json
rules:
disable: [] # e.g. [PQC010] to silence a specific rule
output:
default_format: console # console | sarif | cbom | json | markdown
cbom_path: cbom.jsonNotes:
- CLI flags override config values (e.g.
-s highbeatsseverity_threshold). rules.disabletakes rule IDs (PQC001…PQC016), case-insensitive.baselineis resolved relative to the config file, so the same config works from any working directory.--no-baselineoverrides it.- Without a config file, the defaults exclude
**/tests/**,**/node_modules/**,**/.venv/**,**/venv/**,**/vendor/**,**/dist/**, and**/build/**(thetestsglob matches test directories at any depth), and the walker never descends into.git,site-packages, caches, or IDE directories. - A malformed
--configfile exits with code2; a malformed discovered config prints a warning and falls back to defaults.
Every finding ships with concrete, NIST-aligned migration guidance and a before/after code example. The high-level mapping:
| Vulnerable today | Migrate to | NIST standard |
|---|---|---|
| RSA key generation / encryption (PQC001/002) | ML-KEM-768 (CRYSTALS-Kyber) | FIPS 203 |
| RSA signatures (PQC003) | ML-DSA-65 (CRYSTALS-Dilithium) | FIPS 204 |
| ECDSA / EC keys (PQC004) | ML-DSA-65 | FIPS 204 |
| ECDH / ECDHE / X25519 (PQC005) | ML-KEM-768 | FIPS 203 |
| Ed25519 / Ed448 (PQC006) | ML-DSA-65 (or SLH-DSA) | FIPS 204 / 205 |
| Diffie-Hellman (PQC007) | ML-KEM-768 | FIPS 203 |
| DSA (PQC008) | ML-DSA-65 | FIPS 204 |
| SHA-1 (PQC009) | SHA-256 / SHA3-256 | FIPS 180-4 / 202 |
| MD5 (PQC010) | SHA-256 / SHA3-256 | FIPS 180-4 / 202 |
| Weak JWT RS/ES/PS256 (PQC011) | HS256 internally; track IETF JOSE for PQC | — |
| Weak TLS config (PQC012) | TLS 1.3 + hybrid X25519MLKEM768 | FIPS 203 |
| DES / 3DES (PQC013) | AES-256-GCM | FIPS 197 |
| Stored key material (PQC015) | ML-KEM-768 / ML-DSA-65 — inventory, reissue, revoke | FIPS 203 / 204 |
| Certificates (PQC016) | ML-DSA-65 or hybrid/composite certificates | FIPS 204 |
Heads-up about liboqs. The Python post-quantum library
oqs(liboqs-python) is not a purepip install. It wraps the compiled liboqs C library, which must be available first:pip install liboqsbuilds it from source via CMake, or you install the distro package / build from https://github.com/open-quantum-safe/liboqs. Hash and symmetric replacements (SHA-256, AES-256-GCM) need no special install — they live inhashliband thecryptographypackage.
pqc-scan uses two complementary detection strategies:
-
Code (Python, JavaScript/TypeScript, Java, Go): tree-sitter AST analysis. Source is parsed into a concrete syntax tree and the analyzers traverse real nodes — call expressions, imports, dotted attribute access, string and keyword arguments. This is not regex matching: it resolves what is actually being called (e.g.
rsa.generate_private_key(...),ec.generate_private_key(...)), so it sees through aliasing and whitespace, reports precise 1-based line/column positions, and avoids matching the same construct inside comments or strings. -
Configs and dependencies: pattern-based scanning. TLS/JWT/cipher settings in YAML/JSON/TOML/
.conf, and manifests likerequirements.txtandpackage.json, are matched against curated patterns of quantum-vulnerable algorithms and libraries.
Each language analyzer exposes a common contract (LANGUAGE, EXTENSIONS,
GRAMMAR, analyze(...)) and emits Finding objects through a single shared
factory, so console, SARIF, CBOM, and JSON outputs all stay perfectly consistent.
pqc-scan is a static analyzer. It is tuned for high-confidence, low-noise
detection, but a few caveats apply:
- It flags presence, not necessarily exploitable risk. A
PQC014dependency match means a quantum-vulnerable library is declared, not that a vulnerable code path is exercised. Treat findings as an inventory to triage. - Dynamic / reflective crypto can be missed. Algorithms chosen at runtime, built from string concatenation, or invoked through heavy indirection may not be resolved by static analysis.
- Detection is best-effort across libraries. Coverage targets the most common crypto APIs per language; exotic or in-house wrappers may not be recognized.
- Configuration heuristics are pattern-based and can occasionally over- or
under-match unusual config layouts. Tune with
rules.disableandexclude. - Rust detection needs a resolvable import. A call whose type does not trace
back to a known crate through a
usedeclaration is not flagged — deliberately, since a localSha1type is as likely as the crate. - Encrypted private keys are skipped. Their algorithm cannot be determined without the passphrase, and guessing from the PEM label would be wrong as often as right.
- Lock files are not scanned. They restate the manifest plus the whole transitive closure; the direct declaration in the manifest is the actionable one.
- Inline suppressions are text-matched, so a
pqc-scan: ignoredirective inside a string literal also suppresses.
Use --changed-only for fast PR feedback, the full scan for an inventory/CBOM, and
the migration guidance attached to each finding as your remediation checklist.
See CHANGELOG.md. The current release is 0.2.1.
Apache-2.0. See the project metadata in pyproject.toml.