pqblocks-lint
Static analysis for Python source that finds cryptographic primitives and says which ones a quantum computer would break (RSA, ECDSA/ECC, X25519) and which it would not (ML-KEM, AES-256, HKDF).
Use it to build a first inventory of the cryptography in a Python codebase as a CycloneDX 1.6 CBOM or a SARIF 2.1.0 log, as one input to a post-quantum migration plan.
How it works: it parses the code with Python's ast module (the code is never executed), builds a data-flow graph, follows values from key-generation calls to encrypt, sign, exchange and derive calls, and matches the result against a fixed table of name patterns. It is a static-analysis heuristic, not a cryptographic module.
Read Limitations before using the verdict as a CI gate. Ed25519, DSA and finite-field DH are not detected, so a file using only those gets
NO_CRYPTO_DETECTED.
pip install pqblocks-lint
import pathlib, tempfile
from pqblocks_lint import cli
src = pathlib.Path(tempfile.mkdtemp(), "keys.py")
src.write_text("from cryptography.hazmat.primitives.asymmetric import rsa\n"
"def make_key():\n"
" key = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n"
" return key\n")
code = cli.main(["--target", str(src), "--format", "cbom", "--output", str(src.with_suffix(".cbom.json")), "--seed", "0"])
print(code) # 1: quantum-vulnerable cryptography found (RSA-2048)
The same from a shell:
pqblocks-lint analyze --target src/ --format sarif --output crypto.sarif --seed 0; echo $?
Why PQBlocks
- Tested: 188 tests: CLI exit codes, graph construction, taint propagation, classification, CBOM/SARIF output, determinism, and five labelled sample programs.
- Honest about accuracy: BENCHMARKS.md compares the tool, file by file, with a third-party scanner on a 16-file labelled corpus, and lists every miss.
- Licence: Apache-2.0. Pure Python. Analysis needs only
networkxand the standard library.
Usage
Command line
pqblocks-lint [analyze] --target PATH --format {cbom,sarif} --output FILE --seed N [--timeout S] [--threads N]
--targetis a.pyfile or a directory (every*.pybelow it is scanned).--seed(required, ≥ 0) does not change the analysis. It only sets the synthetic CBOM timestamp (2026-01-01T00:00:00Z plusseedseconds) and theserialNumber, so the same input and seed always give byte-identical output.- You can also run
python -m pqblocks_lint.cli.
| Exit code | Verdict | Meaning |
|---|---|---|
| 0 | COMPLIANT_QUANTUM_SAFE or NO_CRYPTO_DETECTED |
No quantum-vulnerable primitive matched |
| 1 | NON_COMPLIANT_VULNERABLE |
A quantum-vulnerable primitive was found |
| 2 | BENIGN_TEST_CODE |
Nothing was detected and the file looks like test code (imports pytest/unittest and defines test*/Test*) |
| 3 | n/a | Internal error, e.g. a single target file with a syntax error |
| 4 | n/a | Invalid arguments (missing option, negative seed, path does not exist) |
| 5 | n/a | The analysis took longer than --timeout (checked after it finishes) |
| 6 | n/a | Directory scan only: at least one .py file could not be analysed (syntax error, undecodable, unreadable). Each is printed to stderr as scan error: ...; the output file holds the results for the other files. Takes precedence over 0, 1 and 2 |
In a directory scan, the verdict is NON_COMPLIANT_VULNERABLE if any file is vulnerable.
Files that fail to parse are reported on stderr and make the run exit 6; they are never counted as clean.
Output
- CBOM (
--format cbom): a CycloneDXspecVersion1.6 JSON document. Each detected algorithm is acryptographic-assetcomponent withcryptoProperties.- It adds two fields of its own: a top-level
verdict, andcryptoProperties.detectionContext, which carriesshorVulnerable,cnsa20Compliantandfips1403Status. - Keys are sorted.
- It adds two fields of its own: a top-level
- SARIF (
--format sarif): a SARIF version 2.1.0 log (OASIS Standard, 27 March 2020, with Errata 01 of 28 August 2023).$schemaishttps://json.schemastore.org/sarif-2.1.0.json.- One rule per algorithm, with IDs of the form
PQBLOCKS-LINT/RSA-2048. - Results have level
errorfor quantum-vulnerable algorithms andnotefor quantum-safe ones. Every result points at line 1.
- One rule per algorithm, with IDs of the form
API reference
All signatures are from the code.
| Module | Function | Purpose |
|---|---|---|
pqblocks_lint.cli |
main(argv: list[str] | None = None) -> int |
Run the CLI and return the exit code |
parse_cli_arguments(argv=None) -> LintArgs |
Parse and validate arguments (raises SystemExit(4)) |
|
validate_args(args) -> str | None |
Error message, or None |
|
LintArgs(target, format="cbom", output, seed=0, timeout=30, threads=1) |
Frozen dataclass with .is_valid |
|
pqblocks_lint.graph |
build_cfg_cg(source_path: str) -> networkx.DiGraph |
Parse one file into a data-flow graph. Raises FileNotFoundError or SyntaxError |
get_source_nodes(graph), get_sink_nodes(graph) |
Node IDs | |
get_taint_closure(graph, sources), find_tainted_paths(graph, sources, sinks) |
Reachability helpers | |
pqblocks_lint.taint |
propagate_taint(graph, sources: set[str], taint_filter=None) -> set[str] |
Nodes reachable from the sources |
propagate_taint_interprocedural(graph, sources, call_graph=None) |
Also follows call edges | |
get_tainted_sources(graph, tainted_nodes), classify_taint(graph, tainted_nodes) |
Source / intermediate / sink roles | |
pqblocks_lint.classifier |
classify(graph, tainted_nodes=None) -> list[FIPSParams] |
One entry per detected algorithm family |
classify_compliance(graph, tainted_nodes=None) -> dict |
Summary: is_quantum_safe, nist_level, families, compliance |
|
FIPSParams |
Frozen dataclass: algorithm, fips_id, nist_level, key_size_bits, quantum_safe, compliance, family |
|
pqblocks_lint.serialize |
build_cbom_dict(assets, target="unknown", seed=0, verdict=None) -> dict |
CBOM, with the verdict computed when None |
build_sarif_dict(assets, target="unknown", seed=0) -> dict |
SARIF log | |
emit_outputs(assets, fmt, output_path, target="unknown", seed=0, verdict=None) -> str |
Write cbom or sarif. Raises ValueError for any other format |
|
pqblocks_lint.runner |
run_instance(instance_path, seed=0) -> dict |
Analyse one file and check that the output is deterministic |
run_all_instances(suite_path, seed=0, output_path=None) -> dict |
Run a labelled suite (instances.json) |
The package also contains pqblocks_lint.bench. It is a small harness that times third-party QUBO solvers (dwave-neal, OpenJij, OR-Tools CP-SAT) on a fixed 4-variable problem. It plays no part in the analysis, which is why those solvers appear as dependencies. Its numbers say nothing about this tool.
Limitations
False "safe" verdicts. These are the most important limitations:
- File names no longer affect the verdict.
BENIGN_TEST_CODEis given only when nothing was detected; a finding is never downgraded because of a test-like name or path (atests/directory is not treated specially, so RSA in test code fails the gate). - ML-KEM is reported only from a real API: an import of
kyber_py,mlkem,ml_kemorpqcrypto.kem.ml_kem*/kyber*, or a callKeyEncapsulation("ML-KEM-...")/KeyEncapsulation("Kyber...")(liboqs-python). Aliased imports ofKeyEncapsulationand other PQC libraries are not recognised. A file is called hybrid only if it has both such a call/import and a classical primitive. - A golden sample that fakes a KEM with
os.urandomis now reported as vulnerable (X25519 only).
Detection.
- Detection uses name patterns (
generate_private_key,encrypt,sign,exchange,derive, and names such asrsa,ecdsa,x25519,aes,hkdf,ml_kem). Imports are not resolved. - Ed25519, Ed448, X448, DSA and finite-field DH are not detected.
- ECDH over P-256 is reported as
RSA-3072+X25519-253, becauseec.generate_private_keymatches the RSA key-generation pattern. - RSA key size is read only from a literal
key_size=keyword inside a function body. Otherwise RSA is reported as 3072 bits.
CBOM fields.
sourceLine/sinkLineare always 0 (line numbers are not measured).oidis emitted only for RSA (RSAES-OAEP), ML-KEM-768, ML-DSA-65, X25519 and AES-256-GCM; other algorithms omit it. It is a per-algorithm value, not derived from the detected parameters.- 2048-bit RSA is always named
RSA-2048-OAEP-SHA256. - The CBOM timestamp is synthetic.
Compliance labels.
- CNSA 2.0 requires ML-KEM-1024, ML-DSA-87 and AES-256. The tool labels ML-KEM-768 and ML-DSA as "Compliant (CNSA 2.0 / OMB M-23-02)".
- OMB M-23-02 is an inventory memo, not an algorithm list.
- Treat these labels as hints, not as an assessment.
Scope.
- Python source only.
- Measured accuracy covers the 16-file corpus in BENCHMARKS.md. The false-positive and false-negative rates on real repositories are not measured.
- Analysis time was measured only up to 608 lines per file.
Options that do nothing. --timeout is checked only after the scan ends, and --threads is accepted but ignored.
Security notes
The package reads source files and writes one output file. It never imports or executes the code it scans, and it handles no keys. Its main risk is a wrong verdict; see SECURITY_NOTES.md.
Verification
TESTING.md · BENCHMARKS.md · EVIDENCE.md · VERIFICATION_REPORT.md · USE_CASES.md
Part of PQBlocks
PQBlocks builds open, tested building blocks for the post-quantum transition. This package is its Python cryptography-inventory scanner.
Metadata
Release files for pqblocks-lint 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pqblocks_lint-0.1.0.tar.gz | 49.2 kB | Without provenance | Details for pqblocks_lint-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pqblocks_lint-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pqblocks_lint-0.1.0-py3-none-any.whl |
Total release size: 81.4 kB
Release files / pqblocks_lint-0.1.0.tar.gz
| Download URL | pqblocks_lint-0.1.0.tar.gz |
|---|---|
| Size | 49.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ed06481c7d5dc66746ff50423f98f95a34f64dadbf4a2ab4106d3a523887a10b
|
|
BLAKE2b-256 checksum How to use checksums |
a74d04ab4da9d19cb95351a4818286c692373792a58539d2551549dd6d4c827a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pqblocks_lint-0.1.0-py3-none-any.whl
| Download URL | pqblocks_lint-0.1.0-py3-none-any.whl |
|---|---|
| Size | 32.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
51308cf5ab0a5897a07202765501284a08c25c9dc1b64c28094924d453a39250
|
|
BLAKE2b-256 checksum How to use checksums |
aed5e83a30d0ff11625d04014ad6e478e4c40218ba5e76273761707eca00a63a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for pqblocks-lint 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.