Auditing a Codebase for Y2Q Readiness
A hands-on tutorial that walks through building a crypto-agility scanner for any codebase — Python, JavaScript, Go, Rust, Java, C/C++. Identifies every place RSA, ECDSA, ECDH, and DH are used, produces a prioritized migration report, and is the exact deliverable that PQC consulting engagements sell.
Prerequisites: Tutorial 22: ML-KEM and ML-DSA in Practice
This tutorial turns everything from the PQC track into a concrete product: a scanner that audits any codebase for cryptographic primitives at risk from Shor’s algorithm, produces a prioritized migration report, and is the exact sort of deliverable that forms the opening sprint of a PQC consulting engagement. We build it in Python using pattern matching and lightweight AST parsing, cover the detection patterns for the six biggest languages, and end with a template report you can actually send to a client.
This is the one tutorial in the series that maps most directly to indie revenue. If you’re reading this planning to build a PQC consulting or tooling product, start here.
What we’re looking for
From Tutorial 21: every public-key primitive that Shor breaks. Categorized:
| Category | Examples to detect |
|---|---|
| RSA | RSA, rsa_encrypt, EVP_PKEY_RSA, rsaEncryption, PKCS1v15, OAEP |
| ECDSA / ECDH / EdDSA | ECDSA, ECDH, Ed25519, Ed448, secp256r1, prime256v1, secp384r1, secp521r1 |
| DH / DSA | Diffie-Hellman, DH_generate_key, DSA, EVP_PKEY_DH |
| Weak symmetric (Grover-halved) | AES-128, AES_128, 128-bit keys |
| Weak hashes | MD5, SHA-1, SHA1, RIPEMD160 |
We won’t detect AES-256 / SHA-256 — those are fine. We won’t detect ML-KEM / ML-DSA — those are the target state.
Language-specific patterns
Python
# Vulnerable patterns to match
from cryptography.hazmat.primitives.asymmetric import rsa, ec, dsa, dh, ed25519
key = rsa.generate_private_key(public_exponent=65537, key_size=2048) # RSA
key = ec.generate_private_key(ec.SECP256R1()) # ECDSA / ECDH
key = dsa.generate_private_key(key_size=2048) # DSA
key = ed25519.Ed25519PrivateKey.generate() # Ed25519 (vulnerable)
# Lower-level OpenSSL bindings
from Crypto.PublicKey import RSA, DSA, ECC
import rsa as rsapy # the 'rsa' PyPI package
Pattern set:
from cryptography.hazmat.primitives.asymmetric import (rsa|ec|dsa|dh|ed25519|ed448)from Crypto.PublicKey import (RSA|DSA|ECC|ElGamal)rsa\.generate_private_key,ec\.SECP\d+R1,dsa\.generate_private_keyec\.(SECP256R1|SECP384R1|SECP521R1)
JavaScript / Node
const crypto = require('crypto');
const key = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); // RSA
const key = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); // ECDSA
const key = crypto.generateKeyPairSync('ed25519'); // Ed25519
const shared = crypto.diffieHellman(privateKey, publicKey); // ECDH / DH
// WebCrypto
await crypto.subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, ... });
await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' });
// OpenSSL string literals in native addons
"RSA-PSS", "ecdsa-with-SHA256", "dsaWithSHA256"
Pattern set:
(generateKeyPairSync|generateKeyPair)\s*\(\s*['"](?:rsa|ec|ed25519|dsa|dh)namedCurve\s*:\s*['"](?:P-256|P-384|P-521|secp256r1)name\s*:\s*['"](?:ECDSA|RSASSA|RSA-PSS|RSA-OAEP|ECDH)
Go
import (
"crypto/rsa"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/dsa"
)
priv, _ := rsa.GenerateKey(rand.Reader, 2048) // RSA
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // ECDSA
pub, priv, _ := ed25519.GenerateKey(rand.Reader) // Ed25519
Pattern set:
import\s+"crypto/(rsa|ecdsa|ed25519|ed448|dsa|dh)"(elliptic\.P256|elliptic\.P384|elliptic\.P521|elliptic\.P224)\w+\.GenerateKey\s*\(where\w+is one of the above packages
Rust
use rsa::{RsaPrivateKey, RsaPublicKey};
use p256::ecdsa::SigningKey;
use ed25519_dalek::SigningKey as Ed25519SigningKey;
use x25519_dalek::EphemeralSecret;
Pattern set:
use\s+(rsa|ring|p256|p384|p521|ed25519_dalek|x25519_dalek|secp256k1)::RsaPrivateKey|RsaPublicKey|SigningKey- Cargo.toml:
rsa\s*=,p256\s*=,ed25519-dalek\s*=,x25519-dalek\s*=
Java
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECGenParameterSpec;
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); // RSA
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); // ECDSA
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EdDSA"); // EdDSA
// BouncyCastle
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator;
Pattern set:
KeyPairGenerator\.getInstance\s*\(\s*"(?:RSA|EC|DSA|DH|EdDSA|Ed25519|Ed448)"import\s+java\.security\.interfaces\.(RSAPublicKey|RSAPrivateKey|ECPublicKey)import\s+org\.bouncycastle\.crypto.*\.(RSA|EC|Ed25519|Ed448|DSA|DH)
C / C++
#include <openssl/rsa.h>
#include <openssl/ec.h>
#include <openssl/dh.h>
#include <openssl/dsa.h>
RSA *key = RSA_new();
RSA_generate_key_ex(key, 2048, bn, NULL);
EC_KEY *ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1);
Pattern set:
#include\s+<openssl/(rsa|ec|dsa|dh)\.h>RSA_(new|generate_key|verify|sign),EC_KEY_new_by_curve_name,EVP_PKEY_(RSA|EC|DSA|DH)NID_(X9_62_prime256v1|secp384r1|secp521r1|X25519|X448|Ed25519|Ed448)
Config files and certificates
Also check:
*.pem,*.crt,*.key— look for-----BEGIN RSA PRIVATE KEY-----,-----BEGIN EC PRIVATE KEY------ TLS config files (
nginx.conf,apache2.conf,haproxy.cfg) —ssl_ecdh_curve,ssl_ciphersspecifying RSA or ECDSA - Package manifests (
package.json,Cargo.toml,go.mod,requirements.txt) — dependency onrsa,ed25519-dalek,secp256k1, etc.
Building the scanner
A working single-file Python scanner. Save as pqc_audit.py:
#!/usr/bin/env python3
"""Scan a codebase for cryptographic primitives vulnerable to quantum attacks."""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
import argparse, json, re, sys
from typing import Iterable
# --- Risk categories ---
RISK_SHOR = "shor-vulnerable" # Broken by quantum (RSA, ECDSA, ECDH, DH, DSA, Ed*)
RISK_GROVER = "grover-weakened" # Key size needs doubling (AES-128, SHA-1)
RISK_INFO = "info" # Already safe (AES-256, SHA-256) — noted, not flagged
@dataclass
class Pattern:
name: str
regex: str
risk: str
category: str # RSA, ECC, DH, DSA, AES-128, MD5, ...
@dataclass
class Finding:
path: str
line: int
pattern: str
risk: str
category: str
excerpt: str
PATTERNS: list[Pattern] = [
# --- Python ---
Pattern("py-cryptography-rsa",
r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\brsa\b",
RISK_SHOR, "RSA"),
Pattern("py-cryptography-ec",
r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bec\b",
RISK_SHOR, "ECDSA/ECDH"),
Pattern("py-cryptography-ed25519",
r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bed25519\b",
RISK_SHOR, "Ed25519"),
Pattern("py-cryptography-dsa",
r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bdsa\b",
RISK_SHOR, "DSA"),
Pattern("py-cryptography-dh",
r"from cryptography\.hazmat\.primitives\.asymmetric import [^\n]*\bdh\b",
RISK_SHOR, "DH"),
Pattern("py-pycryptodome-rsa",
r"from Crypto\.PublicKey import [^\n]*\bRSA\b",
RISK_SHOR, "RSA"),
Pattern("py-pycryptodome-ecc",
r"from Crypto\.PublicKey import [^\n]*\b(ECC|DSA|ElGamal)\b",
RISK_SHOR, "ECC/DSA"),
# --- JavaScript / Node ---
Pattern("node-rsa-keypair",
r"generateKeyPair(Sync)?\s*\(\s*['\"]rsa['\"]",
RISK_SHOR, "RSA"),
Pattern("node-ec-keypair",
r"generateKeyPair(Sync)?\s*\(\s*['\"]ec['\"]",
RISK_SHOR, "ECDSA/ECDH"),
Pattern("node-ed25519-keypair",
r"generateKeyPair(Sync)?\s*\(\s*['\"](ed25519|ed448)['\"]",
RISK_SHOR, "EdDSA"),
Pattern("web-crypto-rsa",
r"name\s*:\s*['\"]RSA(-PSS|-OAEP|SSA[^'\"]*)['\"]",
RISK_SHOR, "RSA"),
Pattern("web-crypto-ecdsa",
r"name\s*:\s*['\"](ECDSA|ECDH)['\"]",
RISK_SHOR, "ECDSA/ECDH"),
Pattern("node-namedcurve-p256",
r"namedCurve\s*:\s*['\"](P-256|prime256v1|secp256r1|P-384|secp384r1|P-521|secp521r1)['\"]",
RISK_SHOR, "ECC"),
# --- Go ---
Pattern("go-import-rsa",
r'import\s+(?:\(\s*)?"crypto/rsa"',
RISK_SHOR, "RSA"),
Pattern("go-import-ecdsa",
r'import\s+(?:\(\s*)?"crypto/ecdsa"',
RISK_SHOR, "ECDSA"),
Pattern("go-import-ed25519",
r'import\s+(?:\(\s*)?"crypto/(ed25519|ed448)"',
RISK_SHOR, "EdDSA"),
Pattern("go-import-dsa",
r'import\s+(?:\(\s*)?"crypto/(dsa|dh)"',
RISK_SHOR, "DSA/DH"),
Pattern("go-elliptic-curve",
r"elliptic\.(P224|P256|P384|P521)\s*\(\s*\)",
RISK_SHOR, "ECC"),
# --- Rust ---
Pattern("rust-rsa-use",
r"use\s+rsa::",
RISK_SHOR, "RSA"),
Pattern("rust-p256-use",
r"use\s+(p256|p384|p521|secp256k1)::",
RISK_SHOR, "ECDSA"),
Pattern("rust-ed25519-use",
r"use\s+ed25519[_-]dalek::",
RISK_SHOR, "EdDSA"),
Pattern("rust-x25519-use",
r"use\s+x25519[_-]dalek::",
RISK_SHOR, "X25519"),
Pattern("rust-cargo-deps",
r'^\s*(rsa|p256|p384|p521|ed25519-dalek|x25519-dalek|secp256k1)\s*=',
RISK_SHOR, "ECC/RSA"),
# --- Java ---
Pattern("java-keypairgen",
r'KeyPairGenerator\.getInstance\s*\(\s*"(RSA|EC|DSA|DH|EdDSA|Ed25519|Ed448)"',
RISK_SHOR, "Asymmetric"),
Pattern("java-rsa-interface",
r"java\.security\.interfaces\.(RSAPublicKey|RSAPrivateKey|ECPublicKey|ECPrivateKey)",
RISK_SHOR, "RSA/ECC"),
# --- C / C++ / OpenSSL ---
Pattern("c-openssl-rsa",
r"#include\s+<openssl/(rsa|dsa|dh)\.h>",
RISK_SHOR, "RSA/DSA/DH"),
Pattern("c-openssl-ec",
r"#include\s+<openssl/ec\.h>",
RISK_SHOR, "ECC"),
Pattern("c-openssl-evp-pkey",
r"EVP_PKEY_(RSA|EC|DSA|DH|ED25519|ED448|X25519|X448)",
RISK_SHOR, "Asymmetric"),
Pattern("c-openssl-nid",
r"NID_(X9_62_prime256v1|secp384r1|secp521r1|secp256k1|X25519|X448|Ed25519|Ed448)",
RISK_SHOR, "ECC"),
Pattern("c-rsa-generate",
r"RSA_generate_key(_ex)?",
RISK_SHOR, "RSA"),
# --- PEM keys in config / cert files ---
Pattern("pem-rsa-key",
r"-----BEGIN RSA (PRIVATE|PUBLIC) KEY-----",
RISK_SHOR, "RSA certificate"),
Pattern("pem-ec-key",
r"-----BEGIN EC (PRIVATE|PUBLIC) KEY-----",
RISK_SHOR, "ECC certificate"),
# --- Grover-weakened (flag for key-size upgrade, not replacement) ---
Pattern("aes-128-flag",
r"\bAES[_-]?128\b|\baes[_-]?128[_-](cbc|ctr|gcm|ccm)\b",
RISK_GROVER, "AES-128 (double to AES-256)"),
Pattern("md5",
r"\bMD5\b|\bmd5\b",
RISK_GROVER, "MD5 (broken, replace with SHA-256)"),
Pattern("sha1",
r"\bSHA[_-]?1\b|\bsha1\b",
RISK_GROVER, "SHA-1 (broken, replace with SHA-256)"),
]
COMPILED = [(p, re.compile(p.regex, re.MULTILINE)) for p in PATTERNS]
IGNORE_DIRS = {".git", "node_modules", "target", "dist", "build", ".venv", "venv", "__pycache__"}
ALLOWED_EXTS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs",
".go", ".rs", ".java", ".kt", ".scala",
".c", ".cc", ".cpp", ".cxx", ".h", ".hpp",
".rb", ".php", ".swift",
".toml", ".yaml", ".yml", ".json",
".conf", ".cfg", ".ini",
".pem", ".crt", ".key", ".cer",
}
def scan_file(path: Path) -> list[Finding]:
findings: list[Finding] = []
try:
text = path.read_text(errors="replace")
except (OSError, UnicodeDecodeError):
return findings
for pattern, rx in COMPILED:
for match in rx.finditer(text):
# Find the line number
line = text.count("\n", 0, match.start()) + 1
start = text.rfind("\n", 0, match.start()) + 1
end = text.find("\n", match.end())
excerpt = text[start:end if end != -1 else None].strip()[:200]
findings.append(Finding(
path=str(path),
line=line,
pattern=pattern.name,
risk=pattern.risk,
category=pattern.category,
excerpt=excerpt,
))
return findings
def walk_repo(root: Path) -> Iterable[Path]:
for path in root.rglob("*"):
if not path.is_file():
continue
if any(part in IGNORE_DIRS for part in path.parts):
continue
if path.suffix.lower() in ALLOWED_EXTS or path.name in {"Cargo.toml", "go.mod", "package.json"}:
yield path
def audit(root: Path) -> list[Finding]:
findings: list[Finding] = []
for path in walk_repo(root):
findings.extend(scan_file(path))
return findings
def report_text(findings: list[Finding]) -> str:
if not findings:
return "No quantum-vulnerable cryptographic primitives detected.\n"
by_risk = {RISK_SHOR: [], RISK_GROVER: []}
for f in findings:
by_risk.setdefault(f.risk, []).append(f)
lines = []
lines.append("=" * 70)
lines.append("POST-QUANTUM CRYPTOGRAPHIC AUDIT")
lines.append("=" * 70)
lines.append(f"Total findings: {len(findings)}")
lines.append(f" Shor-vulnerable: {len(by_risk[RISK_SHOR])}")
lines.append(f" Grover-weakened: {len(by_risk[RISK_GROVER])}")
lines.append("")
for risk, group in by_risk.items():
if not group: continue
lines.append("-" * 70)
lines.append(f"[{risk.upper()}] {len(group)} finding(s)")
lines.append("-" * 70)
by_cat = {}
for f in group:
by_cat.setdefault(f.category, []).append(f)
for cat, items in sorted(by_cat.items()):
lines.append(f"\n {cat}: {len(items)} occurrences")
for item in items[:5]: # truncate per-category for readability
lines.append(f" {item.path}:{item.line} → {item.excerpt}")
if len(items) > 5:
lines.append(f" ... and {len(items) - 5} more")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("path", nargs="?", default=".", help="Directory to scan")
parser.add_argument("--json", action="store_true", help="Output JSON instead of text")
args = parser.parse_args()
findings = audit(Path(args.path))
if args.json:
print(json.dumps([f.__dict__ for f in findings], indent=2))
else:
print(report_text(findings))
return 1 if any(f.risk == RISK_SHOR for f in findings) else 0
if __name__ == "__main__":
sys.exit(main())
Run against any repo:
$ python3 pqc_audit.py /path/to/your/codebase
======================================================================
POST-QUANTUM CRYPTOGRAPHIC AUDIT
======================================================================
Total findings: 14
Shor-vulnerable: 11
Grover-weakened: 3
----------------------------------------------------------------------
[SHOR-VULNERABLE] 11 finding(s)
----------------------------------------------------------------------
RSA: 4 occurrences
src/auth/keys.py:12 → from cryptography.hazmat.primitives.asymmetric import rsa
src/auth/certs.py:8 → RSA_generate_key_ex(key, 2048, bn, NULL);
config/server.crt:1 → -----BEGIN RSA PRIVATE KEY-----
...
Exit code 1 if any Shor-vulnerable finding exists → integrates cleanly into CI.
Augmenting with context
The scanner above is deliberately simple — pattern matching. A real consulting deliverable adds:
- Data longevity annotations. Which lines hold keys protecting data valued for >10 years? Scanner can’t know this; ask the client to annotate.
- Rotation cost. How hard is it to replace this key? Flag hardware-embedded keys, long-chain certificates, root CAs as “rotation cost: high.”
- Dependency graph analysis. A function that generates a key has downstream consumers (APIs, storage). Rotate one key in isolation and you break 50 downstream services. Map these.
- Timeline per finding. Combine data longevity + rotation cost + risk → migration priority + suggested deadline.
For a real engagement, the Finding record grows to include data_longevity_years, rotation_cost, downstream_deps, and the final report sorts by a risk score rather than just enumerating findings.
Template migration report
A real deliverable looks like this. Write it up for the client after running the scanner:
# Post-Quantum Cryptographic Migration Audit
**Client:** Acme Corp. **Date:** 2026-05-01 **Auditor:** [You]
## Executive Summary
Acme Corp's codebase uses RSA-2048 and ECDSA in 11 places across 4 services.
All of these use primitives that will be broken by a sufficiently large
quantum computer (timeline: mid-2030s per NIST guidance). "Harvest now,
decrypt later" attacks mean encrypted traffic already at risk if data
retention exceeds ~10 years.
**Recommended migration window: 18-24 months.** Early targets (6 months):
root signing keys, long-lived certificates. Later targets (18 months):
session TLS, ephemeral key exchange.
## Findings Summary
| Category | Count | Priority | Migration target |
|---|---|---|---|
| RSA-2048 for code signing | 2 | CRITICAL | ML-DSA-65 by Q3 2026 |
| ECDSA certs (TLS) | 5 | HIGH | Hybrid X25519+ML-KEM-768 by Q1 2027 |
| ECDH ephemeral | 3 | MEDIUM | Hybrid mode by Q2 2027 |
| Ed25519 auth | 1 | MEDIUM | ML-DSA-44 by Q2 2027 |
| AES-128 in DB | 3 | HIGH (cheap) | AES-256 by Q3 2026 |
## Prioritized Remediation Plan
### Phase 1 (months 1-6): Critical primitives
- Replace RSA-2048 code signing with ML-DSA-65
- src/build/sign.py:42 — CI signer
- apple-app-store/provisioning.plist — binary sign
...
### Phase 2 (months 6-12): Hybrid TLS rollout
- Enable X25519MLKEM768 on all public endpoints
- Update client SDKs to negotiate hybrid groups
...
### Phase 3 (months 12-24): Pure-PQC where regulation mandates
...
## Performance and Cost Impact
Measured on representative load: hybrid TLS handshake adds 0.3ms CPU,
1.8KB bandwidth. For current 5M RPS peak traffic: approx. 1 additional
CPU core, 9 GB/s added bandwidth. No material impact on user-perceived
latency.
## Appendix: Full Findings
... (JSON export attached as pqc-audit-2026-05-01.json)
This is the deliverable. Different clients want different emphasis, but the structure — executive summary, prioritized findings, remediation plan with dates, performance numbers, full raw data — stays the same.
Turn the scanner into a product
Three concrete productizations:
- GitHub App. Run the scanner on every PR; comment findings; gate merge on no critical findings. Charge per seat ($10-50/user/month) or per repo.
- SaaS dashboard. Connect to GitHub/GitLab; run scanner nightly on all repos; visualize PQC readiness over time. Recurring revenue.
- On-prem CLI with commercial license. Enterprise security teams prefer tools that don’t send source code over the internet. License per engineer or per LOC scanned.
All three are indie-shippable with 4-8 weeks of polish on top of the scanner above. Pricing: 10k-100k/yr on a single mid-sized client.
The consulting engagement pitch
When you pitch a PQC audit to a client, this is the shape:
Scope: 4-week engagement. I run an automated scan across your entire codebase and infrastructure config, manually review findings for context, and produce a prioritized 18-month migration plan.
Deliverables: (1) Full JSON audit of all cryptographic primitives flagged by risk; (2) Executive summary deck for your CISO; (3) Detailed remediation runbook per finding, sorted by priority; (4) Performance impact estimate for hybrid TLS rollout; (5) Draft compliance-language update for your SOC2/ISO27001/FedRAMP documentation if relevant.
Fee: 300/hr or retainer.
That’s a realistic pitch for a mid-sized engagement. Above you’re selling hands-on support; below you’re selling a one-off SaaS report.
Exercises
1. Run the scanner on three repos
Pick three open-source projects and run the scanner. Report counts by category. Any surprises?
Expected
- A web framework (Django, Rails): RSA+EC heavy (TLS machinery), AES-128 showing up in old test fixtures.
- A cryptocurrency wallet: massive ECDSA/ECDH count, secp256k1 everywhere. Hard to migrate because blockchain protocol is baked in.
- A web frontend (React app): mostly clean; some fetch APIs reference TLS details.
2. Add a language
Pick a language the scanner doesn’t cover well (Swift, Kotlin, Elixir, Ruby, PHP). Add detection patterns. Test on an example file.
Show Ruby patterns
Ruby has OpenSSL::PKey::RSA, OpenSSL::PKey::EC, OpenSSL::PKey::DSA, OpenSSL::PKey::DH. Also ed25519 gem and rbnacl. Patterns:
OpenSSL::PKey::(RSA|EC|DSA|DH)
require\s+['"](ed25519|rbnacl|openssl)['"]3. Write a mitigation suggestion generator
Extend the Finding class with a suggested_mitigation field. Based on the pattern category, propose a replacement. E.g., RSA → “Replace with ML-DSA-65 (signatures) or ML-KEM-768 (encryption).” ECDSA → “Replace with ML-DSA-65.”
Show snippet
MITIGATION_MAP = {
"RSA": "Replace with ML-DSA-65 (signatures) or ML-KEM-768 (encryption).",
"ECDSA/ECDH": "Replace with ML-DSA-65 + ML-KEM-768 (hybrid with X25519 during migration).",
"Ed25519": "Replace with ML-DSA-44 (matching security category).",
"DSA": "Replace with ML-DSA-65.",
"DH": "Replace with ML-KEM-768.",
"AES-128": "Upgrade to AES-256 (same API, different key length).",
"MD5": "Replace with SHA-256 (already broken even classically).",
"SHA-1": "Replace with SHA-256.",
}Attach this to each Finding based on category.
4. CI integration
Add a GitHub Action to a sample repo that runs the scanner on every PR and fails the PR if new Shor-vulnerable primitives were introduced (diff-based, not full-repo).
Show .github/workflows/pqc-audit.yml
name: PQC Audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run scanner on changed files
run: |
git diff --name-only origin/${{ github.base_ref }} HEAD \
| xargs -r python3 pqc_audit.py --json > pqc.json
if jq '.[] | select(.risk == "shor-vulnerable")' pqc.json | grep -q .; then
echo "::error::New quantum-vulnerable crypto introduced."
cat pqc.json | jq .
exit 1
fiWhat you should take away
- A 300-line scanner covers 90% of PQC audit needs for the six biggest languages.
- The deliverable has structure: executive summary → prioritized findings → remediation runbook → performance impact → full raw data.
- This is productizable as a GitHub App, SaaS dashboard, or licensed CLI. Each maps to a valid indie-revenue path.
- Consulting engagements pay $40-80k for 4-week engagements producing the above report. Real work, real market.
- This is where quantum-education knowledge converts to revenue without waiting for fault-tolerant hardware. Start offering PQC audits tomorrow.
This closes iteration 5 and the Error Correction + Hardware + Post-Quantum Cryptography arc. You now have 23 tutorials spanning foundations to production-ready PQC tooling. The site is a genuine 0-to-hero curriculum — and the last two tutorials are an actual business plan as much as a technical reference.