README.md
| 1 | # PQC Lint |
| 2 | |
| 3 |  |
| 4 |  |
| 5 |  |
| 6 |  |
| 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 | |