Developer-native scanner for quantum-vulnerable cryptography
Project description
pqc-scan
Snyk for Post-Quantum Cryptography — a developer-first static analysis tool that finds quantum-vulnerable cryptography in your code, configs, and dependencies 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
Why post-quantum, why now
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.
Harvest Now, Decrypt Later
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.
The clock is real
- 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.
Install
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.1.0
For development (tests + coverage):
pip install -e ".[dev]"
pytest
All scanning dependencies (tree-sitter grammars for Python, JavaScript, Java, Go, plus Typer, Rich and PyYAML) are installed automatically — there is no native toolchain to build.
Quickstart
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 . --changed-only # only files changed in the current git diff
pqc-scan rules # list every detection rule
pqc-scan init # write a starter .pqcscan.yml
pqc-scan exposes four commands: scan, report, init, and rules.
scan — scan a path
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. |
--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. |
|
--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
# 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 50
Example 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 — write a report file
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] [--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.sarif
init — scaffold a config
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.yml
rules — list detection rules
pqc-scan rules
Prints a table of every rule (ID, name, severity, category, description).
What it detects
pqc-scan ships 14 rules, covering quantum-vulnerable key generation, signatures,
encryption, key exchange, hashing, weak JWT/TLS configuration, legacy ciphers, and
quantum-vulnerable dependencies.
| 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 |
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.
Supported manifests/configs: requirements.txt, package.json, plus
YAML / JSON / TOML / .conf configuration files (TLS, JWT, cipher lists).
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.
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.
Output formats
Choose with -o/--output on scan, or --format on report.
console (default)
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.
sarif — GitHub code scanning
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.1.0", "rules": [ ... ] } },
"results": [ ... ]
}
]
}
cbom — CycloneDX Cryptography Bill of Materials
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", "...": "..." }
]
}
json — plain JSON
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.1.0",
"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"
}
}
]
}
GitHub Action
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
- name: Run pqc-scan
uses: ./ # this repo's action; or pin: pqc-scan/pqc-scan@v1
with:
path: .
severity: high # critical | high | medium | low
output-sarif: pqc-scan.sarif
fail-on-findings: 'false' # set 'true' to block the PR on any finding
- name: Upload SARIF to GitHub code scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: pqc-scan.sarif
Action inputs
| Input | Default | Description |
|---|---|---|
path |
. |
File or directory to scan. |
severity |
medium |
Minimum severity to report. |
output-sarif |
pqc-scan.sarif |
Path to write the SARIF report. |
fail-on-findings |
false |
Fail the job (exit 1) if any findings are reported. |
The action exits 0 when the scan is clean; with fail-on-findings: 'true' the
SARIF is still uploaded before the job is failed, so annotations always appear.
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 high
Pair it with --changed-only in PR jobs to scan only the diff and keep runs fast.
Configuration
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
scan_configs: true # Scan YAML/JSON/TOML/.conf config files
scan_dependencies: true # Scan dependency manifests (requirements.txt, package.json, ...)
rules:
disable: [] # e.g. [PQC010] to silence a specific rule
output:
default_format: console # console | sarif | cbom | json
cbom_path: cbom.json
Notes:
- CLI flags override config values (e.g.
-s highbeatsseverity_threshold). rules.disabletakes rule IDs (PQC001…PQC014), case-insensitive.- 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.
Migration guidance
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 |
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.
How it works
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.
Limitations & accuracy
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.
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.
License
Apache-2.0. See the project metadata in pyproject.toml.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqc_scan-0.1.0.tar.gz.
File metadata
- Download URL: pqc_scan-0.1.0.tar.gz
- Upload date:
- Size: 85.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
253627ee04a9a9ee72b55a8688bcfa7a69a42743b0839da9e176ba30d0c03d3c
|
|
| MD5 |
9105fd00a510526ef2c0ae7cf56c4b80
|
|
| BLAKE2b-256 |
a6f9218ae2ade0e42229cf530758c4af1cb777a5bb74d0e6f84e8b102da7cf40
|
File details
Details for the file pqc_scan-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pqc_scan-0.1.0-py3-none-any.whl
- Upload date:
- Size: 73.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d962f4ac73e669c4898fa3926011f4c42e69cd714394b6fc7b0927d7f38f26f0
|
|
| MD5 |
f93b992de65a44f5a49cf7e8805877ad
|
|
| BLAKE2b-256 |
9f466a8a29ef0e5fc71c59f3c3aa895ff7ec170bc25b8e9d11a31249209de6b9
|