README.md
10.8 KB · 244 lines · markdown Raw
1 # PQC Lint
2
3 ![PQC Native](https://img.shields.io/badge/PQC-Native-blue)
4 ![ML-DSA / ML-KEM / SLH-DSA](https://img.shields.io/badge/suggests-ML--DSA%20%7C%20ML--KEM%20%7C%20SLH--DSA-green)
5 ![License](https://img.shields.io/badge/License-Apache%202.0-orange)
6 ![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)
7
8 **Static analyzer for classical cryptography.** `pqc-lint` scans your source code for quantum-vulnerable crypto primitives — RSA, ECDSA, Ed25519, DH, ECDH, DSA, MD5, SHA-1 — across **six languages** (Python, JavaScript/TypeScript, Go, Rust, Java/Kotlin, C/C++) and points each finding at the matching NIST PQC replacement (**ML-DSA**, **ML-KEM**, **SLH-DSA**). Ships as both a drop-in **GitHub Action** and a standalone **CLI**. Emits **SARIF 2.1.0** for GitHub code scanning and inline **PR annotations** via workflow commands.
9
10 ## The Problem
11
12 Every RSA keypair, every ECDSA signature, every ECDH handshake in your codebase is a time bomb. Once a cryptographically relevant quantum computer (CRQC) exists, Shor's algorithm breaks all of them. Data encrypted today under RSA-OAEP can be captured now and decrypted later ("harvest-now-decrypt-later"). Migration is not optional — it is a years-long engineering effort, and step one is knowing where the classical crypto actually lives.
13
14 ## The Solution
15
16 `pqc-lint` gives you that inventory. Every PR gets scanned, every finding is mapped to a specific PQC replacement with rationale, and CI fails if critical quantum-vulnerable primitives land on `main`.
17
18 ## Quick Start
19
20 ### As a GitHub Action
21
22 Add `.github/workflows/pqc-lint.yml`:
23
24 ```yaml
25 name: PQC Lint
26
27 on:
28 pull_request:
29 branches: [main]
30 push:
31 branches: [main]
32
33 permissions:
34 contents: read
35 security-events: write
36 pull-requests: write
37
38 jobs:
39 pqc-lint:
40 runs-on: ubuntu-latest
41 steps:
42 - uses: actions/checkout@v4
43 - uses: dyber-pqc/pqc-lint-action@v1
44 with:
45 path: '.'
46 fail-on: 'high'
47 upload-sarif: 'true'
48 ```
49
50 Findings appear as:
51 - Inline PR annotations on the changed lines (via workflow commands)
52 - Entries in the GitHub Security tab (via SARIF upload)
53 - Failed check if any finding is at or above the `fail-on` threshold
54
55 ### As a CLI
56
57 ```bash
58 pip install pqc-lint
59
60 pqc-lint scan ./src
61 pqc-lint scan ./src --format sarif --output results.sarif
62 pqc-lint scan ./src --fail-on high
63 pqc-lint scan ./src --languages python,go
64 pqc-lint rules # list all rules
65 pqc-lint --version
66 ```
67
68 ## Architecture
69
70 ```
71 +--------------------+
72 | CLI (click) |
73 | action_runner |
74 +---------+----------+
75 |
76 v
77 +----------+ +-----+------+ +------------+
78 | file | | | | |
79 path ----->| walker |--- file ----->| Scanner |--- matcher ->| Patterns |
80 +----------+ | | | (per-lang)|
81 excludes +-----+------+ +-----+------+
82 globs | |
83 | Finding | regex hits
84 v v
85 +----+-----+ +------+------+
86 | ScanReport|<------------| Rules |
87 +----+-----+ +-------------+
88 |
89 +----------------+--------------+---------------+----------------+
90 | | | | |
91 v v v v v
92 +---------+ +----------+ +-----------+ +---------+ +-----------+
93 | text | | json | | sarif | | github | | (other) |
94 |(rich) | | | | 2.1.0 | | commds | | |
95 +---------+ +----------+ +-----------+ +---------+ +-----------+
96 ```
97
98 ## Threat Model
99
100 | Adversary capability | pqc-lint claim |
101 | ------------------------------------------ | --------------------------------------------------------------- |
102 | Future CRQC (Shor's algorithm) | Flags *every* known classical public-key primitive in the repo. |
103 | Insider commits RSA without review | CI annotation + failed check at `fail-on: high`. |
104 | Supply-chain slip (new dep uses ECDH) | Regex patterns catch the import/call site on next PR. |
105 | Obfuscated / dynamic crypto | **Not in scope.** Static regex matching; does not evaluate code.|
106 | Binary-only / generated code | **Not in scope.** Source files only. |
107
108 `pqc-lint` is a *detector* — not a remediation tool and not a proof of absence. It catches the common call sites in six languages across the dominant libraries. It is designed to have a low false-negative rate on idiomatic usage and a tolerable false-positive rate. Review each finding.
109
110 ## Rule Catalog
111
112 ### Signatures (broken by Shor's)
113
114 | Rule | Severity | Primitive | Replacement |
115 | ------- | -------- | ----------- | -------------------- |
116 | PQC001 | CRITICAL | RSA signing | ML-DSA-65 (FIPS 204) |
117 | PQC002 | CRITICAL | ECDSA | ML-DSA-65 (FIPS 204) |
118 | PQC003 | HIGH | Ed25519 | ML-DSA-44 / SLH-DSA |
119 | PQC004 | HIGH | DSA | ML-DSA-44 / SLH-DSA |
120
121 ### Key exchange (broken by Shor's)
122
123 | Rule | Severity | Primitive | Replacement |
124 | ------- | -------- | ----------- | -------------------- |
125 | PQC101 | CRITICAL | ECDH | ML-KEM-768 (FIPS 203)|
126 | PQC102 | CRITICAL | DH | ML-KEM-768 (FIPS 203)|
127 | PQC103 | HIGH | X25519 | ML-KEM-512 (FIPS 203)|
128
129 ### Encryption (broken by Shor's)
130
131 | Rule | Severity | Primitive | Replacement |
132 | ------- | -------- | ------------------ | --------------------- |
133 | PQC201 | CRITICAL | RSA-OAEP | ML-KEM-768 (FIPS 203) |
134 | PQC202 | CRITICAL | RSA PKCS#1 v1.5 | ML-KEM-768 (FIPS 203) |
135
136 ### Weak hashes
137
138 | Rule | Severity | Primitive | Replacement |
139 | ------- | -------- | --------- | ---------------------- |
140 | PQC301 | MEDIUM | MD5 | SHA3-256 / SHAKE-256 |
141 | PQC302 | MEDIUM | SHA-1 | SHA3-256 / SHAKE-256 |
142
143 ## Supported Languages and Libraries
144
145 | Language | Extensions | Libraries detected |
146 | -------------------- | ----------------------------------------------- | ------------------------------------------------------------------ |
147 | Python | `.py` | `cryptography`, `pycryptodome`, `ecdsa`, `hashlib` |
148 | JavaScript/TypeScript| `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, `.tsx` | Node `crypto`, Web Crypto API, `node-forge`, `tweetnacl` |
149 | Go | `.go` | `crypto/rsa`, `crypto/ecdsa`, `crypto/ed25519`, `crypto/md5`, etc. |
150 | Rust | `.rs` | `rsa`, `ecdsa`, `ed25519-dalek`, `x25519-dalek`, `ring` |
151 | Java / Kotlin | `.java`, `.kt` | `java.security`, `javax.crypto`, BouncyCastle |
152 | C / C++ | `.c`, `.cc`, `.cpp`, `.cxx`, `.h`, `.hpp` | OpenSSL legacy API + EVP API |
153
154 ## Output Formats
155
156 | Format | Best for | Contents |
157 | -------- | --------------------------- | ------------------------------------------------------------------ |
158 | `text` | local terminal | Rich-formatted table, grouped by file, with snippets and fixes. |
159 | `json` | custom tooling / piping | Schema-v1.0 JSON: scan metadata + `findings[]` with full fields. |
160 | `sarif` | GitHub code scanning | SARIF 2.1.0: rules catalog + results. Upload via `upload-sarif`. |
161 | `github` | inside GitHub Actions | `::error`, `::warning`, `::notice` workflow commands — PR inline. |
162
163 ## `fail-on` severity semantics
164
165 The action (or CLI) exits non-zero if **any** finding has severity **>=** the `fail-on` threshold.
166
167 | `fail-on` | Fails CI when |
168 | ---------- | ---------------------------------------------------------------------------------- |
169 | `critical` | A CRITICAL finding exists (RSA/ECDSA signing, ECDH, DH, RSA-OAEP). |
170 | `high` | *(default)* A CRITICAL or HIGH finding exists (adds Ed25519, DSA, X25519). |
171 | `medium` | Adds MD5 / SHA-1. |
172 | `low` | Any finding at all. |
173 | `info` | Any finding at all, including info-level annotations. |
174
175 ## Excluded by default
176
177 ```
178 **/.git/**
179 **/node_modules/**
180 **/__pycache__/**
181 **/.venv/**
182 **/venv/**
183 **/dist/**
184 **/build/**
185 **/.pytest_cache/**
186 **/.ruff_cache/**
187 **/*.min.js
188 ```
189
190 Pass more globs via `exclude:` on the action or `--exclude` on the CLI.
191
192 ## API Reference
193
194 ```python
195 from pqc_lint import Scanner, Severity
196
197 scanner = Scanner(languages=("python", "go"))
198 report = scanner.scan_path("./src")
199
200 print(report.counts_by_severity())
201 # {'critical': 3, 'high': 1, 'medium': 2, 'low': 0, 'info': 0}
202
203 if report.has_failing(Severity.HIGH):
204 raise SystemExit(1)
205
206 for f in report.findings:
207 print(f.rule_id, f.file, f.line, f.suggestion)
208 ```
209
210 Reporters:
211
212 ```python
213 from pqc_lint.reporters import SarifReporter, JsonReporter, TextReporter
214
215 sarif_text = SarifReporter().render(report)
216 json_text = JsonReporter().render(report)
217 text_out = TextReporter().render(report)
218 ```
219
220 ## Development
221
222 ```bash
223 cd tools/pqc-lint-action
224 pip install -e ".[dev]"
225 pytest -v
226 ruff check src/ tests/
227 ```
228
229 ## Contributing
230
231 Issues and PRs welcome. When adding a new rule or pattern:
232
233 1. Add the `Rule` entry in `src/pqc_lint/rules.py` with an appropriate ID range.
234 2. Add the regex pattern(s) to the per-language matcher(s) in `src/pqc_lint/patterns/`.
235 3. Add a test in `tests/test_scanner_<language>.py` that writes a minimal vulnerable file and asserts the rule fires.
236
237 ## License
238
239 Apache 2.0. See `LICENSE`.
240
241 ## Related
242
243 Part of the [QuantaMrkt](https://quantamrkt.com) open-source tools registry — a catalog of post-quantum security tooling.
244