pqcheck 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/bin/pqcheck.js +62 -14
- package/package.json +14 -4
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# pqcheck
|
|
2
|
+
|
|
3
|
+
> **Public Surface Blast Radius scanner** — find out how much of your data unlocks when quantum decryption arrives.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx pqcheck chase.com
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That's it. No install. Works from any terminal with Node 18+.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
`pqcheck` scans any HTTPS domain and computes its **Decryption Blast Radius Score** — the first continuous metric for harvest-now-decrypt-later (HNDL) risk. Every other TLS scanner answers "is post-quantum cryptography enabled?" with a yes/no. `pqcheck` answers the question that actually matters: *if an adversary harvests this traffic today and decrypts it in 2035, how much past + future data unlocks?*
|
|
16
|
+
|
|
17
|
+
The score combines:
|
|
18
|
+
- **Cipher-class probing** — does the server accept RSA fallback even if it prefers ECDHE?
|
|
19
|
+
- **Certificate chain analysis** — including the intermediate cert (the chain's actual quantum failure point)
|
|
20
|
+
- **Public-key reuse across rotations** — detects when the same private key has been live across multiple cert renewals (often 4+ years at large enterprises)
|
|
21
|
+
- **Subject scale** — wildcard certs and subdomain count multiplying the blast radius
|
|
22
|
+
|
|
23
|
+
## Example
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
$ npx pqcheck chase.com
|
|
27
|
+
|
|
28
|
+
chase.com
|
|
29
|
+
─────────────────────────────────────
|
|
30
|
+
PUBLIC SURFACE BLAST RADIUS: 5.6 / 10 (MEDIUM)
|
|
31
|
+
|
|
32
|
+
Public surface signals:
|
|
33
|
+
• TLS: TLSv1.3 (TLS_AES_128_GCM_SHA256)
|
|
34
|
+
• Hybrid PQC: no
|
|
35
|
+
• Cert expires: in 127 days
|
|
36
|
+
• HSTS: not detected
|
|
37
|
+
• Subdomains: 47 (wildcard cert)
|
|
38
|
+
|
|
39
|
+
Findings:
|
|
40
|
+
[HIGH] Same RSA-2048 key reused for 4.2 years across 3 cert rotations
|
|
41
|
+
[HIGH] ECDHE-only — quantum-vulnerable key exchange
|
|
42
|
+
[MED] Wildcard cert spans 47 subdomains
|
|
43
|
+
|
|
44
|
+
⚠ This is the PUBLIC surface only.
|
|
45
|
+
Internal Blast Radius is typically 12–40× the public score.
|
|
46
|
+
|
|
47
|
+
Plain-English impact:
|
|
48
|
+
If quantum decryption arrives in 2030–2040, harvested traffic from
|
|
49
|
+
chase.com (US banks) would unlock 4.2 years of session data, across
|
|
50
|
+
47 subdomains under one wildcard cert.
|
|
51
|
+
|
|
52
|
+
→ Full report: https://quantapact.com/?check=chase.com
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
npx pqcheck <domain> Scan and print human-readable report
|
|
59
|
+
npx pqcheck <domain> --format json Output raw JSON for piping / scripting
|
|
60
|
+
npx pqcheck <domain> --threshold 7 Exit 2 if score ≥ 7 (CI gate)
|
|
61
|
+
npx pqcheck <domain> --quiet Print only the numeric score
|
|
62
|
+
npx pqcheck --help Show all options
|
|
63
|
+
npx pqcheck --version Show version
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Exit codes
|
|
67
|
+
|
|
68
|
+
| Code | Meaning |
|
|
69
|
+
|------|---------|
|
|
70
|
+
| 0 | Success — score below threshold (or no threshold set) |
|
|
71
|
+
| 1 | Usage / network / scan error |
|
|
72
|
+
| 2 | Score met or exceeded `--threshold` |
|
|
73
|
+
|
|
74
|
+
### CI integration
|
|
75
|
+
|
|
76
|
+
The `--threshold` flag turns `pqcheck` into a quantum-risk gate for any pipeline:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
# .github/workflows/quantum-risk-gate.yml
|
|
80
|
+
- name: Check public-surface quantum-decryption risk
|
|
81
|
+
run: npx pqcheck mycompany.com --threshold 7
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If the score is 7.0 or higher, the step fails and the PR can't merge.
|
|
85
|
+
|
|
86
|
+
For finer control, combine `--quiet` with shell logic:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
SCORE=$(npx pqcheck mybank.com --quiet)
|
|
90
|
+
echo "Public surface blast radius: $SCORE / 10"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or grab the full JSON for richer analysis:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx pqcheck mybank.com --format json | jq '.findings[] | select(.severity=="high")'
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Web version
|
|
100
|
+
|
|
101
|
+
Same scanner, browser-friendly UI: **[quantapact.com](https://quantapact.com)**
|
|
102
|
+
|
|
103
|
+
Shareable per-domain reports at `quantapact.com/r/<domain>` — the URL unfurls in Twitter / Slack / LinkedIn with a dynamically-generated card showing the grade.
|
|
104
|
+
|
|
105
|
+
## Public leaderboard
|
|
106
|
+
|
|
107
|
+
Sector rankings updated nightly across:
|
|
108
|
+
|
|
109
|
+
- US Banks (20 peers)
|
|
110
|
+
- US Healthcare Systems (20 peers)
|
|
111
|
+
- Major SaaS / Cloud Platforms (30 peers)
|
|
112
|
+
- US Federal Government (25 peers)
|
|
113
|
+
- Major EU & UK Banks (25 peers)
|
|
114
|
+
- US Defense Contractors (15 peers)
|
|
115
|
+
- Global Automakers (15 peers)
|
|
116
|
+
- Global News & Media (15 peers)
|
|
117
|
+
- US Telecom & ISPs (15 peers)
|
|
118
|
+
- US Airlines (10 peers)
|
|
119
|
+
- UK Government & Public Services (15 peers)
|
|
120
|
+
|
|
121
|
+
→ **[quantapact.com/leaderboard.html](https://quantapact.com/leaderboard.html)**
|
|
122
|
+
|
|
123
|
+
## Methodology
|
|
124
|
+
|
|
125
|
+
The Decryption Blast Radius scoring methodology is fully open and documented at **[quantapact.com/methodology](https://quantapact.com/methodology)**. Citable; methodology paper coming soon.
|
|
126
|
+
|
|
127
|
+
## Privacy
|
|
128
|
+
|
|
129
|
+
`pqcheck` sends the domain you scan to the Quantapact API (so the actual TLS handshake can be performed from the public internet). No other data is sent — no email, no IP-tied identifier, nothing client-side. The server-side `/api/scan` endpoint stores anonymous scan results in a 30-minute cache to avoid duplicate scans of the same domain. See [quantapact.com/privacy](https://quantapact.com/privacy) for full details.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT. © 2026 Quantapact.
|
|
134
|
+
|
|
135
|
+
## Disclaimer
|
|
136
|
+
|
|
137
|
+
`pqcheck` measures only the **public** surface of a domain — what's observable from the open internet. Internal Blast Radius (east-west traffic, internal databases, VPN tunnels, backup pipelines) is typically 12–40× the public score depending on sector. A passing public-surface grade does **not** mean low internal exposure.
|
package/bin/pqcheck.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// =============================================================================
|
|
8
8
|
|
|
9
9
|
const API_BASE = process.env.PQCHECK_API_BASE || "https://quantapact.com";
|
|
10
|
-
const VERSION = "0.
|
|
10
|
+
const VERSION = "0.2.0";
|
|
11
11
|
|
|
12
12
|
const ANSI = {
|
|
13
13
|
reset: "\x1b[0m",
|
|
@@ -41,16 +41,22 @@ async function main() {
|
|
|
41
41
|
process.exit(1);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const
|
|
44
|
+
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
45
|
+
const format = parseFormat(args);
|
|
46
|
+
const threshold = parseThreshold(args);
|
|
47
|
+
if (threshold === "invalid") {
|
|
48
|
+
console.error(color("red", "error: --threshold requires a number 0-10"));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
45
51
|
|
|
46
|
-
process.stderr.write(color("dim", `Scanning ${domain} ...`));
|
|
52
|
+
if (!quiet) process.stderr.write(color("dim", `Scanning ${domain} ...`));
|
|
47
53
|
let report;
|
|
48
54
|
try {
|
|
49
55
|
const resp = await fetch(`${API_BASE}/api/scan?domain=${encodeURIComponent(domain)}`, {
|
|
50
56
|
method: "GET",
|
|
51
57
|
headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION}` },
|
|
52
58
|
});
|
|
53
|
-
process.stderr.write("\r\x1b[K"); // clear "Scanning ..." line
|
|
59
|
+
if (!quiet) process.stderr.write("\r\x1b[K"); // clear "Scanning ..." line
|
|
54
60
|
if (!resp.ok) {
|
|
55
61
|
const errBody = await safeJSON(resp);
|
|
56
62
|
console.error(color("red", `error: ${resp.status} ${errBody?.error || resp.statusText}`));
|
|
@@ -59,17 +65,45 @@ async function main() {
|
|
|
59
65
|
}
|
|
60
66
|
report = await resp.json();
|
|
61
67
|
} catch (err) {
|
|
62
|
-
process.stderr.write("\r\x1b[K");
|
|
68
|
+
if (!quiet) process.stderr.write("\r\x1b[K");
|
|
63
69
|
console.error(color("red", `error: ${err.message}`));
|
|
64
70
|
process.exit(1);
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
if (
|
|
73
|
+
if (quiet) {
|
|
74
|
+
// Numeric score on stdout, nothing else. Suitable for piping.
|
|
75
|
+
console.log(typeof report.score === "number" ? report.score : "");
|
|
76
|
+
} else if (format === "json") {
|
|
68
77
|
console.log(JSON.stringify(report, null, 2));
|
|
69
|
-
|
|
78
|
+
} else {
|
|
79
|
+
printReport(report);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
// Threshold gating: exit 2 if score >= threshold (distinct from exit 1 = error)
|
|
83
|
+
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
84
|
+
if (!quiet) {
|
|
85
|
+
console.error(color("red", `threshold breach: score ${report.score} >= ${threshold}`));
|
|
86
|
+
}
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseFormat(args) {
|
|
93
|
+
if (args.includes("--json")) return "json"; // back-compat alias
|
|
94
|
+
const i = args.indexOf("--format");
|
|
95
|
+
if (i === -1) return "text";
|
|
96
|
+
const v = (args[i + 1] || "").toLowerCase();
|
|
97
|
+
return v === "json" ? "json" : "text";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseThreshold(args) {
|
|
101
|
+
const i = args.indexOf("--threshold");
|
|
102
|
+
if (i === -1) return null;
|
|
103
|
+
const raw = args[i + 1];
|
|
104
|
+
const n = Number(raw);
|
|
105
|
+
if (!Number.isFinite(n) || n < 0 || n > 10) return "invalid";
|
|
106
|
+
return n;
|
|
73
107
|
}
|
|
74
108
|
|
|
75
109
|
function printReport(r) {
|
|
@@ -137,6 +171,8 @@ function printReport(r) {
|
|
|
137
171
|
}
|
|
138
172
|
|
|
139
173
|
console.log(color("violet", ` → Full report: ${API_BASE}/?check=${encodeURIComponent(r.domain)}`));
|
|
174
|
+
console.log(color("dim", ` → Share this: ${API_BASE}/r/${encodeURIComponent(r.domain)}`));
|
|
175
|
+
console.log(color("dim", ` → Compare two: ${API_BASE}/compare?a=${encodeURIComponent(r.domain)}&b=`));
|
|
140
176
|
console.log("");
|
|
141
177
|
}
|
|
142
178
|
|
|
@@ -161,17 +197,29 @@ ${color("bold", "pqcheck")} ${color("dim", `v${VERSION}`)}
|
|
|
161
197
|
Public Surface Blast Radius — quantum-decryption risk for any domain.
|
|
162
198
|
|
|
163
199
|
${color("bold", "Usage:")}
|
|
164
|
-
npx pqcheck <domain>
|
|
165
|
-
npx pqcheck <domain> --json
|
|
200
|
+
npx pqcheck <domain> Scan and print human-readable report
|
|
201
|
+
npx pqcheck <domain> --format json Output raw JSON
|
|
202
|
+
npx pqcheck <domain> --threshold 7 Exit 2 if score ≥ 7 (CI-friendly)
|
|
203
|
+
npx pqcheck <domain> --quiet Print just the score, nothing else
|
|
166
204
|
|
|
167
205
|
${color("bold", "Options:")}
|
|
168
|
-
-h, --help
|
|
169
|
-
-v, --version
|
|
170
|
-
--json
|
|
206
|
+
-h, --help Show this help
|
|
207
|
+
-v, --version Show version
|
|
208
|
+
--format <text|json> Output format (default: text)
|
|
209
|
+
--json Alias for --format json
|
|
210
|
+
--threshold <0-10> Exit code 2 if score meets/exceeds this value
|
|
211
|
+
-q, --quiet Print only the numeric score (pipe-friendly)
|
|
212
|
+
|
|
213
|
+
${color("bold", "Exit codes:")}
|
|
214
|
+
0 success
|
|
215
|
+
1 usage / network / scan error
|
|
216
|
+
2 score met or exceeded --threshold
|
|
171
217
|
|
|
172
218
|
${color("bold", "Examples:")}
|
|
173
219
|
npx pqcheck chase.com
|
|
174
|
-
npx pqcheck quantapact.com --json
|
|
220
|
+
npx pqcheck quantapact.com --format json
|
|
221
|
+
npx pqcheck mybank.com --threshold 7 ${color("dim", "# fail CI if exposed")}
|
|
222
|
+
echo "score: $(npx pqcheck mybank.com -q)"
|
|
175
223
|
|
|
176
224
|
Backed by the patented Decryption Blast Radius methodology.
|
|
177
225
|
${color("violet", "https://quantapact.com")}
|
package/package.json
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Decryption Blast Radius scanner — find out how much of your data unlocks when quantum decryption arrives.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"post-quantum",
|
|
7
7
|
"cryptography",
|
|
8
8
|
"security",
|
|
9
9
|
"tls",
|
|
10
|
+
"ssl",
|
|
10
11
|
"scanner",
|
|
11
12
|
"harvest-now-decrypt-later",
|
|
12
13
|
"hndl",
|
|
13
14
|
"blast-radius",
|
|
14
|
-
"pqc"
|
|
15
|
+
"pqc",
|
|
16
|
+
"quantum",
|
|
17
|
+
"crypto-audit",
|
|
18
|
+
"crypto-inventory"
|
|
15
19
|
],
|
|
16
20
|
"homepage": "https://quantapact.com",
|
|
17
21
|
"bugs": "https://quantapact.com",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/mzon7/quantapact.git",
|
|
25
|
+
"directory": "cli"
|
|
26
|
+
},
|
|
18
27
|
"license": "MIT",
|
|
19
28
|
"author": "Quantapact",
|
|
20
29
|
"type": "module",
|
|
@@ -25,6 +34,7 @@
|
|
|
25
34
|
"pqcheck": "./bin/pqcheck.js"
|
|
26
35
|
},
|
|
27
36
|
"files": [
|
|
28
|
-
"bin/"
|
|
37
|
+
"bin/",
|
|
38
|
+
"README.md"
|
|
29
39
|
]
|
|
30
40
|
}
|