PQC readiness scanner: TLS endpoints, source code, and certificate files
Project description
pqready
Post-quantum cryptography readiness scanner. Audits TLS endpoints, source code, and certificate files for quantum-vulnerable crypto and surfaces a prioritised migration plan — as a CLI, a library, an MCP server, or a remote Cloudflare-hosted tool.
Install
pip install pqready
Requires Python 3.11+. Installs two console scripts:
| Command | What it is |
|---|---|
pqready |
the audit CLI |
pqready-mcp |
MCP server over stdio (Claude Code, Cursor, Cline) |
Quickstart
# Scan a TLS endpoint
pqready tls api.example.com
# Scan a source tree
pqready source ./my-service
# Inspect a certificate file
pqready cert /etc/ssl/certs/api.example.com.pem
# Auto-detect target type
pqready scan ./my-service
pqready scan api.example.com
# Generate an HTML report alongside the terminal summary
pqready tls api.example.com --output report.html
Exit codes: 0 clean · 1 CRITICAL/HIGH findings · 2 scan error.
Plays cleanly with CI: just call pqready and let the exit code fail the job.
What it detects
pqready looks for three classes of quantum-relevant weakness.
TLS endpoints — backed by sslyze
| ID | Detects | Severity |
|---|---|---|
| TLS-001 | TLS 1.0 / 1.1 still supported | HIGH |
| TLS-002 | RSA key exchange (no PFS) — harvest-now-decrypt-later | CRITICAL |
| TLS-003 | RC4 / 3DES / NULL / EXPORT cipher accepted | CRITICAL |
| TLS-004 | Certificate uses RSA < 3072 bits | CRITICAL |
| TLS-005 | Certificate uses ECC (P-256 / P-384 / …) | HIGH |
| TLS-006 | Certificate uses RSA ≥ 3072 bits (not yet PQC) | MEDIUM |
| TLS-007 | X25519MLKEM768 hybrid PQC KEX advertised | INFO ✓ |
| TLS-008 | Certificate expires in < 30 days | HIGH |
| TLS-009 | Certificate signed with MD5 or SHA-1 | CRITICAL |
Source code — AST for Python, regex for config
| ID | Detects | Severity |
|---|---|---|
| SRC-001 | import Crypto / from Crypto… (PyCrypto / PyCryptodome) |
HIGH |
| SRC-002 | from cryptography.hazmat.primitives.asymmetric import rsa |
HIGH |
| SRC-003 | RSA.generate(<3072) |
CRITICAL |
| SRC-004 | ec.generate_private_key / EllipticCurvePrivateKey |
HIGH |
| SRC-005 | hashlib.md5( / hashlib.sha1( |
MEDIUM |
| SRC-006 | Hardcoded RSA / AES-128-CBC in config files |
LOW |
| SRC-007 | .pem / .crt / .key referenced via open(…) |
LOW |
Skips binary files and files > 1 MB. Prunes .git, .venv, node_modules,
__pycache__, dist, build and friends.
Certificate files — PEM / DER / CRT / CER / KEY
Reuses the cert-level checks above (TLS-004 … TLS-009) and applies them to files on disk. Useful for offline audits of leaf certs and private keys.
Sample terminal output
pqready · TLS scan · api.example.com:443
────────────────────────────────────────
✗ CRITICAL TLS-002 RSA key exchange offered (no perfect forward secrecy)
✗ CRITICAL TLS-004 RSA certificate with 2048-bit key
✗ HIGH TLS-005 ECC certificate (secp256r1)
✓ INFO TLS-007 No hybrid PQC key exchange offered
Risk score: 80/100 · PQC-ready: NO
────────────────────────────────────────
Run with --output report.html to generate a full report.
The HTML report is self-contained (no external CSS / JS, no remote fetches). Open it in a browser or attach it to a ticket as-is.
Library usage
from pqready.core.tls import scan_tls
from pqready.core.source import scan_source
from pqready.core.certs import scan_cert_file
from pqready.reporters.html import render_html
tls_result = scan_tls("api.example.com")
src_results = scan_source("./services")
cert_result = scan_cert_file("./api.example.com.pem")
render_html([tls_result, *src_results, cert_result], "report.html")
Every scanner returns a ScanResult:
@dataclass
class Finding:
id: str # e.g. "TLS-002"
category: FindingCategory
severity: Severity # critical | high | medium | low | info
title: str
description: str
location: str # endpoint URL, file path, or "file:line"
evidence: str
remediation: str
nist_ref: str # e.g. "FIPS 203 (ML-KEM)"
pqc_ready: bool
@dataclass
class ScanResult:
target: str
scan_type: str # "tls" | "source" | "cert"
findings: list[Finding]
scanned_at: str # ISO 8601
duration_ms: int
error: str | None
risk_score: int # 0-100, severity-weighted
pqc_ready: bool # False if any CRITICAL/HIGH
MCP server
pqready ships an MCP server (pqready-mcp) over stdio that exposes four
tools any MCP-capable agent (Claude Code, Cursor, Cline, …) can call:
| Tool | Input | Returns |
|---|---|---|
scan_tls_endpoint |
hostname, port (=443) |
ScanResult dict |
scan_source_code |
path |
{"results": [ScanResult]} |
scan_cert_file |
path |
ScanResult dict |
generate_html_report |
results_json, output_path |
{output_path, targets} |
Wire it in by adding to your MCP client config:
{
"mcpServers": {
"pqready": { "command": "pqready-mcp" }
}
}
Full tool schemas, example payloads, and a decision tree for which tool to
call live in skill/SKILL.md — drop that file into any
agent skill loader to teach the agent how to use pqready correctly.
Cloudflare Worker (remote MCP)
A thin proxy Worker lives in cf_worker/. It handles bearer
auth and streams MCP traffic to a backend running pqready-mcp over HTTP/SSE
(Cloud Run, Fly, VPS, etc.). The Python runtime cannot execute directly in a
Worker, so this layer is auth + proxy only.
cd cf_worker
npm install
wrangler secret put PQREADY_API_TOKEN
npm run deploy
See cf_worker/README.md for the full setup.
Interpreting findings
risk_score is a coarse summary, not a substitute for reading the
findings. Two endpoints can hit 100 for entirely different reasons.
pqc_ready is intentionally conservative: it returns False whenever any
CRITICAL or HIGH finding exists. A green run only means we did not find any
of the patterns we know to look for — it is not a certification.
Every finding carries:
- NIST reference — most often FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), SP 800-52, or SP 800-131A.
- Remediation — concrete, copy-pasteable next step.
See skill/SKILL.md for the full per-ID remediation table.
Documentation
| Where | What |
|---|---|
skill/SKILL.md |
Full MCP tool schemas + agent decision tree |
cf_worker/README.md |
Cloudflare Worker deployment guide |
| GitHub repo | Issues, releases, source |
| PyPI project | Latest release, install command |
Development
git clone https://github.com/pqready/pqready.git
cd pqready
python -m venv .venv && source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -e ".[dev]"
ruff check .
pytest tests/
CI runs ruff + pytest on Python 3.11 and 3.12. Releases are cut by pushing a
v* tag; .github/workflows/publish.yml
publishes to PyPI via OIDC trusted publishing — no API tokens stored in the
repo.
Contributing
Bug reports and PRs welcome. For non-trivial changes, please open an issue
first so we can align on scope. Run ruff check . and pytest tests/
before opening a PR.
Limitations
- TLS scans depend on the handshake
sslyzeobserves. Some load balancers hide cipher details from external probes. - The source scanner is pattern-based and will miss findings hidden behind
dynamic dispatch (e.g.
getattr(crypto_mod, name)). - PQC group detection matches OpenSSL-style names. Servers exposing only IANA codepoints in custom log formats may report differently.
pqreadydoes not test certificate revocation or CT-log inclusion.
License
Apache-2.0 — see LICENSE.
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 pqready-0.2.2.tar.gz.
File metadata
- Download URL: pqready-0.2.2.tar.gz
- Upload date:
- Size: 34.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf1eb2098e30157d521df664ce34540f4452b888c72612617e13b84852833e03
|
|
| MD5 |
abb26608ff23fcd290bf2d038b00094d
|
|
| BLAKE2b-256 |
9f6f3ad992b3bf0e181ecf0f9d2d788ccc85c37a8aab6818a1e98125f9eeafd5
|
Provenance
The following attestation bundles were made for pqready-0.2.2.tar.gz:
Publisher:
publish.yml on pqready/pqready
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pqready-0.2.2.tar.gz -
Subject digest:
cf1eb2098e30157d521df664ce34540f4452b888c72612617e13b84852833e03 - Sigstore transparency entry: 1575633837
- Sigstore integration time:
-
Permalink:
pqready/pqready@9fb5bab650fa2f44f4492790e24d5faf61dd3dbd -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/pqready
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9fb5bab650fa2f44f4492790e24d5faf61dd3dbd -
Trigger Event:
push
-
Statement type:
File details
Details for the file pqready-0.2.2-py3-none-any.whl.
File metadata
- Download URL: pqready-0.2.2-py3-none-any.whl
- Upload date:
- Size: 28.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6619bad1f168bb2dc9824b7893ccdd39a2c13fd546d5653003690d7f608d5df1
|
|
| MD5 |
94a4a75aca874252b4b61fd4f7591f59
|
|
| BLAKE2b-256 |
0338db6abba10f20e40421dbd42b04ae1707c3cf2793449e9418f3decc9f3de9
|
Provenance
The following attestation bundles were made for pqready-0.2.2-py3-none-any.whl:
Publisher:
publish.yml on pqready/pqready
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pqready-0.2.2-py3-none-any.whl -
Subject digest:
6619bad1f168bb2dc9824b7893ccdd39a2c13fd546d5653003690d7f608d5df1 - Sigstore transparency entry: 1575633866
- Sigstore integration time:
-
Permalink:
pqready/pqready@9fb5bab650fa2f44f4492790e24d5faf61dd3dbd -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/pqready
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9fb5bab650fa2f44f4492790e24d5faf61dd3dbd -
Trigger Event:
push
-
Statement type: