pqcheck 0.6.0 → 0.7.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.
Files changed (2) hide show
  1. package/bin/pqcheck.js +402 -5
  2. package/package.json +1 -1
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.6.0";
10
+ const VERSION = "0.7.0";
11
11
 
12
12
  const ANSI = {
13
13
  reset: "\x1b[0m",
@@ -41,10 +41,38 @@ async function main() {
41
41
  if (args[0] === "deps") {
42
42
  return runDepsCommand(args.slice(1));
43
43
  }
44
+ if (args[0] === "diff") {
45
+ return runDiffCommand(args.slice(1));
46
+ }
47
+ if (args[0] === "history") {
48
+ return runHistoryCommand(args.slice(1));
49
+ }
50
+ if (args[0] === "cert") {
51
+ return runCertCommand(args.slice(1));
52
+ }
53
+
54
+ // Multi-domain support: positional args are domains.
55
+ // --file reads additional domains from a newline-delimited file.
56
+ const fileFlagIdx = args.indexOf("--file");
57
+ let fileDomains = [];
58
+ if (fileFlagIdx >= 0) {
59
+ const filePath = args[fileFlagIdx + 1];
60
+ if (!filePath) {
61
+ console.error(color("red", "error: --file requires a path argument"));
62
+ process.exit(1);
63
+ }
64
+ try {
65
+ const fs = await import("node:fs/promises");
66
+ const raw = await fs.readFile(filePath, "utf8");
67
+ fileDomains = raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
68
+ } catch (err) {
69
+ console.error(color("red", `error reading --file ${filePath}: ${err.message}`));
70
+ process.exit(1);
71
+ }
72
+ }
44
73
 
45
- // Multi-domain support: any non-flag positional arg is a domain
46
74
  const positional = args.filter((a) => !a.startsWith("-") && !isFlagValue(args, a));
47
- const domains = positional
75
+ const domains = [...positional, ...fileDomains]
48
76
  .map((a) => normalizeDomain(a))
49
77
  .filter((d) => !!d);
50
78
 
@@ -140,6 +168,10 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi
140
168
  printCsvRow(report);
141
169
  } else if (format === "markdown") {
142
170
  printMarkdown(report, multi);
171
+ } else if (format === "sarif") {
172
+ console.log(JSON.stringify(reportToSarif(report), null, 2));
173
+ } else if (format === "gh-action") {
174
+ printGitHubActionAnnotations(report);
143
175
  } else {
144
176
  if (multi) console.log(color("dim", `\n──── ${domain} ────`));
145
177
  printReport(report);
@@ -228,15 +260,18 @@ function isFlagValue(args, val) {
228
260
  const idx = args.indexOf(val);
229
261
  if (idx <= 0) return false;
230
262
  const prev = args[idx - 1];
231
- return prev === "--threshold" || prev === "--format" || prev === "--watch" || prev === "--webhook";
263
+ return prev === "--threshold" || prev === "--format" || prev === "--watch" || prev === "--webhook" || prev === "--file" || prev === "-o" || prev === "--allowlist";
232
264
  }
233
265
 
234
266
  function parseFormat(args) {
235
267
  if (args.includes("--json")) return "json"; // back-compat alias
268
+ if (args.includes("--gh-action")) return "gh-action"; // GitHub Actions annotation format
236
269
  const i = args.indexOf("--format");
237
270
  if (i === -1) return "text";
238
271
  const v = (args[i + 1] || "").toLowerCase();
239
- if (v === "json" || v === "csv" || v === "markdown" || v === "md") return v === "md" ? "markdown" : v;
272
+ if (v === "json" || v === "csv" || v === "markdown" || v === "md" || v === "sarif" || v === "gh-action") {
273
+ return v === "md" ? "markdown" : v;
274
+ }
240
275
  return "text";
241
276
  }
242
277
 
@@ -772,6 +807,23 @@ async function runDepsCommand(args) {
772
807
  const maxArg = args.find((a) => a.startsWith("--max="));
773
808
  const maxThirdParties = maxArg ? Math.max(1, parseInt(maxArg.slice(6), 10) || 20) : 20;
774
809
 
810
+ // Allowlist support: --allowlist <path> reads newline-separated host patterns.
811
+ // If a third-party host is NOT in the allowlist, the scan exits non-zero with code 3.
812
+ // Useful as a CI gate for vendor-risk teams: "fail PR if any unapproved third-party appears."
813
+ const allowlistIdx = args.indexOf("--allowlist");
814
+ let allowlist = null;
815
+ if (allowlistIdx >= 0) {
816
+ const allowlistPath = args[allowlistIdx + 1];
817
+ try {
818
+ const fs2 = await import("node:fs/promises");
819
+ const raw = await fs2.readFile(allowlistPath, "utf8");
820
+ allowlist = new Set(raw.split(/\r?\n/).map(l => l.trim().toLowerCase()).filter(l => l && !l.startsWith("#")));
821
+ } catch (err) {
822
+ console.error(color("red", `error reading --allowlist: ${err.message}`));
823
+ process.exit(1);
824
+ }
825
+ }
826
+
775
827
  const positional = args.filter((a) => !a.startsWith("-") && a !== outDir);
776
828
  const domain = positional.length > 0 ? normalizeDomain(positional[0]) : null;
777
829
  if (!domain || !isValidDomain(domain)) {
@@ -916,6 +968,23 @@ async function runDepsCommand(args) {
916
968
  process.exit(1);
917
969
  }
918
970
  }
971
+
972
+ // Allowlist gate: exit non-zero if any third-party host isn't in the allowlist.
973
+ if (allowlist) {
974
+ const violations = results.filter(r => !allowlist.has(r.host));
975
+ if (violations.length > 0) {
976
+ if (!json) {
977
+ console.error("");
978
+ console.error(color("red", ` ✗ Allowlist violation: ${violations.length} third-party origin(s) not in allowlist:`));
979
+ for (const v of violations) console.error(` - ${v.host}`);
980
+ console.error("");
981
+ }
982
+ process.exit(3);
983
+ } else if (!json) {
984
+ console.log(` ${color("green", "✓")} ${color("dim", "All third-party origins on allowlist.")}`);
985
+ console.log("");
986
+ }
987
+ }
919
988
  }
920
989
 
921
990
  async function fetchPageHTML(domain) {
@@ -1045,6 +1114,334 @@ function depsManifestToMarkdown(m) {
1045
1114
  return lines.join("\n");
1046
1115
  }
1047
1116
 
1117
+ // =============================================================================
1118
+ // SARIF + GitHub Action output formats
1119
+ // =============================================================================
1120
+
1121
+ function reportToSarif(report) {
1122
+ // SARIF 2.1.0 minimal schema for security findings.
1123
+ // Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
1124
+ const findings = Array.isArray(report.findings) ? report.findings : [];
1125
+ const sevMap = { critical: "error", high: "error", medium: "warning", low: "note" };
1126
+ return {
1127
+ $schema: "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json",
1128
+ version: "2.1.0",
1129
+ runs: [{
1130
+ tool: {
1131
+ driver: {
1132
+ name: "pqcheck",
1133
+ version: VERSION,
1134
+ informationUri: "https://quantapact.com",
1135
+ rules: findings.map((f, i) => ({
1136
+ id: `pqcheck-${i + 1}`,
1137
+ name: (f.title || "finding").replace(/[^A-Za-z0-9]/g, "_"),
1138
+ shortDescription: { text: f.title || "finding" },
1139
+ fullDescription: { text: f.detail || f.title || "finding" },
1140
+ defaultConfiguration: { level: sevMap[f.severity] || "note" },
1141
+ })),
1142
+ },
1143
+ },
1144
+ results: findings.map((f, i) => ({
1145
+ ruleId: `pqcheck-${i + 1}`,
1146
+ level: sevMap[f.severity] || "note",
1147
+ message: { text: `${f.title || "finding"}${f.detail ? ` — ${f.detail}` : ""}` },
1148
+ locations: [{
1149
+ physicalLocation: {
1150
+ artifactLocation: { uri: `https://${report.domain || ""}` },
1151
+ },
1152
+ }],
1153
+ properties: {
1154
+ domain: report.domain,
1155
+ score: report.score,
1156
+ grade: report.grade,
1157
+ severity: f.severity,
1158
+ },
1159
+ })),
1160
+ properties: {
1161
+ score: report.score,
1162
+ grade: report.grade,
1163
+ domain: report.domain,
1164
+ },
1165
+ }],
1166
+ };
1167
+ }
1168
+
1169
+ function printGitHubActionAnnotations(report) {
1170
+ // GitHub Actions workflow command syntax: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
1171
+ const findings = Array.isArray(report.findings) ? report.findings : [];
1172
+ const sevMap = { critical: "error", high: "error", medium: "warning", low: "notice" };
1173
+ // Top-line score/grade as a notice
1174
+ console.log(`::notice title=Quantapact: ${report.domain}::Grade ${report.grade || "?"} · score ${report.score ?? "?"} / 10`);
1175
+ for (const f of findings) {
1176
+ const cmd = sevMap[f.severity] || "notice";
1177
+ const title = (f.title || "finding").replace(/[\r\n]/g, " ");
1178
+ const msg = (f.detail || f.title || "").replace(/[\r\n]/g, " ").replace(/::/g, ":");
1179
+ console.log(`::${cmd} title=${title}::${msg}`);
1180
+ }
1181
+ }
1182
+
1183
+ // =============================================================================
1184
+ // `pqcheck history` — show recent score history for a domain
1185
+ // =============================================================================
1186
+
1187
+ async function runHistoryCommand(args) {
1188
+ const json = args.includes("--json");
1189
+ const positional = args.filter((a) => !a.startsWith("-"));
1190
+ const domain = positional.length > 0 ? normalizeDomain(positional[0]) : null;
1191
+ if (!domain || !isValidDomain(domain)) {
1192
+ console.error(color("red", "error: pqcheck history requires a valid domain"));
1193
+ console.error(color("dim", "Usage: npx pqcheck history <domain> [--json]"));
1194
+ process.exit(1);
1195
+ }
1196
+ const days = (() => {
1197
+ const i = args.indexOf("--days");
1198
+ if (i === -1) return 90;
1199
+ const n = parseInt(args[i + 1] || "90", 10);
1200
+ return Number.isFinite(n) && n > 0 && n <= 365 ? n : 90;
1201
+ })();
1202
+
1203
+ let h;
1204
+ try {
1205
+ const r = await fetch(`${API_BASE}/api/history?domain=${encodeURIComponent(domain)}&days=${days}`, {
1206
+ headers: { accept: "application/json", "user-agent": `pqcheck-cli/${VERSION} (history)` },
1207
+ });
1208
+ if (!r.ok) {
1209
+ console.error(color("red", `error: ${r.status} ${r.statusText}`));
1210
+ process.exit(1);
1211
+ }
1212
+ h = await r.json();
1213
+ } catch (err) {
1214
+ console.error(color("red", `error: ${err.message}`));
1215
+ process.exit(1);
1216
+ }
1217
+
1218
+ if (json) {
1219
+ console.log(JSON.stringify(h, null, 2));
1220
+ return;
1221
+ }
1222
+
1223
+ const points = Array.isArray(h?.points) ? h.points : [];
1224
+ if (points.length === 0) {
1225
+ console.log(` ${color("violet", domain)} ${color("dim", "·")} no history found`);
1226
+ console.log(color("dim", " (need at least one previous scan; try `npx pqcheck " + domain + "` first)"));
1227
+ return;
1228
+ }
1229
+ const sorted = points.slice().reverse(); // oldest → newest
1230
+ const first = sorted[0].score;
1231
+ const last = sorted[sorted.length - 1].score;
1232
+ const delta = last - first;
1233
+ const min = Math.min(...sorted.map(p => p.score));
1234
+ const max = Math.max(...sorted.map(p => p.score));
1235
+ const trend = Math.abs(delta) < 0.1 ? "→ flat" : delta > 0 ? `↑ +${delta.toFixed(1)} (worsened)` : `↓ ${delta.toFixed(1)} (improved)`;
1236
+
1237
+ console.log("");
1238
+ console.log(` ${color("bold", domain)} ${color("dim", "·")} score history (${days}d, ${sorted.length} samples)`);
1239
+ console.log(` ${color("dim", `range ${min.toFixed(1)} – ${max.toFixed(1)} · trend ${trend}`)}`);
1240
+ console.log("");
1241
+ // Compact ASCII sparkline
1242
+ const range = Math.max(0.1, max - min);
1243
+ const ramp = "▁▂▃▄▅▆▇█";
1244
+ let bar = "";
1245
+ for (const p of sorted) {
1246
+ const idx = Math.min(ramp.length - 1, Math.floor(((p.score - min) / range) * (ramp.length - 1)));
1247
+ bar += ramp[idx];
1248
+ }
1249
+ console.log(` ${color("violet", bar)}`);
1250
+ console.log("");
1251
+ // Tail of recent samples
1252
+ console.log(` ${color("dim", "Recent samples (most-recent first):")}`);
1253
+ for (const p of points.slice(0, 8)) {
1254
+ const date = (p.scannedAt || p.date || "").slice(0, 10);
1255
+ console.log(` ${color("dim", date)} score ${color("bold", p.score?.toFixed(1) ?? "?")} grade ${p.grade ?? "?"}`);
1256
+ }
1257
+ console.log("");
1258
+ }
1259
+
1260
+ // =============================================================================
1261
+ // `pqcheck diff` — diff two QXM lockfiles
1262
+ // =============================================================================
1263
+
1264
+ async function runDiffCommand(args) {
1265
+ const fs = await import("node:fs/promises");
1266
+ const json = args.includes("--json");
1267
+ const positional = args.filter((a) => !a.startsWith("-"));
1268
+ if (positional.length !== 2) {
1269
+ console.error(color("red", "error: pqcheck diff requires two lockfile paths"));
1270
+ console.error(color("dim", "Usage: npx pqcheck diff old.lock new.lock [--json]"));
1271
+ process.exit(1);
1272
+ }
1273
+ let oldLock, newLock;
1274
+ try {
1275
+ oldLock = JSON.parse(await fs.readFile(positional[0], "utf8"));
1276
+ newLock = JSON.parse(await fs.readFile(positional[1], "utf8"));
1277
+ } catch (err) {
1278
+ console.error(color("red", `error reading lockfile: ${err.message}`));
1279
+ process.exit(1);
1280
+ }
1281
+
1282
+ const diff = computeLockDiff(oldLock, newLock);
1283
+ if (json) {
1284
+ console.log(JSON.stringify(diff, null, 2));
1285
+ process.exit(diff.regressed ? 2 : 0);
1286
+ }
1287
+
1288
+ console.log("");
1289
+ console.log(` ${color("bold", "Quantapact lockfile diff")}`);
1290
+ console.log(` ${color("dim", `${positional[0]} → ${positional[1]}`)}`);
1291
+ console.log("");
1292
+ if (diff.scoreChange !== null) {
1293
+ const arrow = diff.scoreChange > 0 ? color("red", "↑") : diff.scoreChange < 0 ? color("green", "↓") : color("dim", "→");
1294
+ const direction = diff.scoreChange > 0 ? "worsened" : diff.scoreChange < 0 ? "improved" : "unchanged";
1295
+ console.log(` Score: ${color("bold", diff.oldScore?.toFixed(1) ?? "?")} → ${color("bold", diff.newScore?.toFixed(1) ?? "?")} ${arrow} ${diff.scoreChange.toFixed(1)} (${direction})`);
1296
+ }
1297
+ if (diff.gradeChange) {
1298
+ const colored = diff.regressed ? color("red", diff.newGrade) : color("green", diff.newGrade);
1299
+ console.log(` Grade: ${diff.oldGrade ?? "?"} → ${colored}`);
1300
+ }
1301
+ if (diff.componentChanges?.length > 0) {
1302
+ console.log("");
1303
+ console.log(` ${color("dim", "Component changes:")}`);
1304
+ for (const c of diff.componentChanges) {
1305
+ const arrow = c.change > 0 ? color("red", "↑") : color("green", "↓");
1306
+ console.log(` ${c.name.padEnd(20, " ")} ${c.before?.toFixed(2) ?? "?"} → ${c.after?.toFixed(2) ?? "?"} ${arrow} ${Math.abs(c.change).toFixed(2)}`);
1307
+ }
1308
+ }
1309
+ if (diff.findingsAdded?.length > 0) {
1310
+ console.log("");
1311
+ console.log(` ${color("red", "+ New findings:")}`);
1312
+ for (const f of diff.findingsAdded) console.log(` [${f.severity}] ${f.title}`);
1313
+ }
1314
+ if (diff.findingsResolved?.length > 0) {
1315
+ console.log("");
1316
+ console.log(` ${color("green", "- Resolved findings:")}`);
1317
+ for (const f of diff.findingsResolved) console.log(` [${f.severity}] ${f.title}`);
1318
+ }
1319
+ console.log("");
1320
+ process.exit(diff.regressed ? 2 : 0);
1321
+ }
1322
+
1323
+ function computeLockDiff(oldLock, newLock) {
1324
+ const oldScore = typeof oldLock?.score === "number" ? oldLock.score : oldLock?.summary?.score;
1325
+ const newScore = typeof newLock?.score === "number" ? newLock.score : newLock?.summary?.score;
1326
+ const oldGrade = oldLock?.grade || oldLock?.summary?.grade;
1327
+ const newGrade = newLock?.grade || newLock?.summary?.grade;
1328
+ const scoreChange = (typeof oldScore === "number" && typeof newScore === "number") ? Math.round((newScore - oldScore) * 100) / 100 : null;
1329
+ const componentChanges = [];
1330
+ const oldComp = oldLock?.components || {};
1331
+ const newComp = newLock?.components || {};
1332
+ for (const k of Object.keys({ ...oldComp, ...newComp })) {
1333
+ const before = typeof oldComp[k]?.contribution === "number" ? oldComp[k].contribution : null;
1334
+ const after = typeof newComp[k]?.contribution === "number" ? newComp[k].contribution : null;
1335
+ if (before !== null && after !== null && Math.abs(after - before) >= 0.05) {
1336
+ componentChanges.push({ name: k, before, after, change: Math.round((after - before) * 100) / 100 });
1337
+ }
1338
+ }
1339
+ // Findings comparison by title
1340
+ const oldFindings = Array.isArray(oldLock?.findings) ? oldLock.findings : [];
1341
+ const newFindings = Array.isArray(newLock?.findings) ? newLock.findings : [];
1342
+ const oldTitles = new Set(oldFindings.map(f => f.title));
1343
+ const newTitles = new Set(newFindings.map(f => f.title));
1344
+ const findingsAdded = newFindings.filter(f => !oldTitles.has(f.title));
1345
+ const findingsResolved = oldFindings.filter(f => !newTitles.has(f.title));
1346
+ const regressed = (typeof scoreChange === "number" && scoreChange > 0.1) || findingsAdded.some(f => f.severity === "high" || f.severity === "critical");
1347
+ return {
1348
+ oldScore, newScore, oldGrade, newGrade, scoreChange,
1349
+ gradeChange: oldGrade !== newGrade,
1350
+ componentChanges,
1351
+ findingsAdded, findingsResolved,
1352
+ regressed,
1353
+ };
1354
+ }
1355
+
1356
+ // =============================================================================
1357
+ // `pqcheck cert <pem-file>` — analyze a local cert file (offline)
1358
+ // =============================================================================
1359
+
1360
+ async function runCertCommand(args) {
1361
+ const fs = await import("node:fs/promises");
1362
+ const crypto = await import("node:crypto");
1363
+ const json = args.includes("--json");
1364
+ const positional = args.filter((a) => !a.startsWith("-"));
1365
+ if (positional.length === 0) {
1366
+ console.error(color("red", "error: pqcheck cert requires a cert file path"));
1367
+ console.error(color("dim", "Usage: npx pqcheck cert <path-to-pem-or-crt> [--json]"));
1368
+ process.exit(1);
1369
+ }
1370
+
1371
+ let pemData;
1372
+ try {
1373
+ pemData = await fs.readFile(positional[0], "utf8");
1374
+ } catch (err) {
1375
+ console.error(color("red", `error reading cert file: ${err.message}`));
1376
+ process.exit(1);
1377
+ }
1378
+
1379
+ let cert;
1380
+ try {
1381
+ cert = new crypto.X509Certificate(pemData);
1382
+ } catch (err) {
1383
+ console.error(color("red", `error parsing cert: ${err.message}`));
1384
+ console.error(color("dim", "Expected a PEM-encoded X.509 cert (.pem, .crt, .cer)."));
1385
+ process.exit(1);
1386
+ }
1387
+
1388
+ const validFrom = new Date(cert.validFrom);
1389
+ const validTo = new Date(cert.validTo);
1390
+ const daysLeft = Math.round((validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
1391
+ const sigAlg = (cert.publicKey?.asymmetricKeyType || "unknown").toUpperCase();
1392
+ const keyBits = cert.publicKey?.asymmetricKeyDetails?.modulusLength || cert.publicKey?.asymmetricKeyDetails?.namedCurve || "?";
1393
+ const sans = (cert.subjectAltName || "").split(",").map(s => s.trim()).filter(Boolean);
1394
+ const isWildcard = sans.some(s => s.includes("*."));
1395
+
1396
+ // Quantum exposure assessment
1397
+ let quantumNote;
1398
+ if (sigAlg === "RSA" && Number(keyBits) >= 2048) {
1399
+ quantumNote = "RSA-" + keyBits + " — broken by Shor's algorithm once a CRQC exists";
1400
+ } else if (sigAlg === "EC" || sigAlg === "ECDSA") {
1401
+ quantumNote = "ECDSA (" + keyBits + ") — broken by Shor's algorithm once a CRQC exists";
1402
+ } else if (sigAlg === "ED25519") {
1403
+ quantumNote = "Ed25519 — broken by Shor's algorithm once a CRQC exists";
1404
+ } else {
1405
+ quantumNote = sigAlg + " — quantum exposure unknown";
1406
+ }
1407
+
1408
+ const result = {
1409
+ file: positional[0],
1410
+ subject: cert.subject,
1411
+ issuer: cert.issuer,
1412
+ serialNumber: cert.serialNumber,
1413
+ validFrom: validFrom.toISOString(),
1414
+ validTo: validTo.toISOString(),
1415
+ daysUntilExpiry: daysLeft,
1416
+ keyAlgorithm: sigAlg,
1417
+ keyBits,
1418
+ sans,
1419
+ isWildcard,
1420
+ isCA: cert.ca,
1421
+ quantumExposure: quantumNote,
1422
+ };
1423
+
1424
+ if (json) {
1425
+ console.log(JSON.stringify(result, null, 2));
1426
+ return;
1427
+ }
1428
+
1429
+ console.log("");
1430
+ console.log(` ${color("bold", "Cert analysis")}: ${color("violet", positional[0])}`);
1431
+ console.log("");
1432
+ console.log(` Subject: ${cert.subject}`);
1433
+ console.log(` Issuer: ${cert.issuer}`);
1434
+ console.log(` Valid: ${validFrom.toISOString().slice(0, 10)} → ${validTo.toISOString().slice(0, 10)} (${daysLeft} days remaining)`);
1435
+ console.log(` Serial: ${cert.serialNumber}`);
1436
+ console.log(` Key: ${sigAlg}-${keyBits}`);
1437
+ console.log(` SANs (${sans.length}): ${sans.slice(0, 4).join(", ")}${sans.length > 4 ? ", ..." : ""}`);
1438
+ console.log(` Wildcard: ${isWildcard ? "yes" : "no"}`);
1439
+ console.log(` CA cert: ${cert.ca ? "yes" : "no"}`);
1440
+ console.log("");
1441
+ console.log(` ${color("yellow", "Quantum exposure:")} ${quantumNote}`);
1442
+ console.log("");
1443
+ }
1444
+
1048
1445
  main().catch((err) => {
1049
1446
  console.error(color("red", `fatal: ${err.message}`));
1050
1447
  process.exit(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqcheck",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
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",