@quantakrypto/core 0.1.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 (102) hide show
  1. package/README.md +337 -0
  2. package/dist/baseline.d.ts +37 -0
  3. package/dist/baseline.d.ts.map +1 -0
  4. package/dist/baseline.js +99 -0
  5. package/dist/baseline.js.map +1 -0
  6. package/dist/cbom.d.ts +25 -0
  7. package/dist/cbom.d.ts.map +1 -0
  8. package/dist/cbom.js +131 -0
  9. package/dist/cbom.js.map +1 -0
  10. package/dist/changed.d.ts +13 -0
  11. package/dist/changed.d.ts.map +1 -0
  12. package/dist/changed.js +67 -0
  13. package/dist/changed.js.map +1 -0
  14. package/dist/config.d.ts +55 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +232 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/cwe.d.ts +16 -0
  19. package/dist/cwe.d.ts.map +1 -0
  20. package/dist/cwe.js +16 -0
  21. package/dist/cwe.js.map +1 -0
  22. package/dist/dependencies.d.ts +25 -0
  23. package/dist/dependencies.d.ts.map +1 -0
  24. package/dist/dependencies.js +276 -0
  25. package/dist/dependencies.js.map +1 -0
  26. package/dist/detect-utils.d.ts +63 -0
  27. package/dist/detect-utils.d.ts.map +1 -0
  28. package/dist/detect-utils.js +113 -0
  29. package/dist/detect-utils.js.map +1 -0
  30. package/dist/detectors/pem.d.ts +9 -0
  31. package/dist/detectors/pem.d.ts.map +1 -0
  32. package/dist/detectors/pem.js +137 -0
  33. package/dist/detectors/pem.js.map +1 -0
  34. package/dist/detectors/source.d.ts +25 -0
  35. package/dist/detectors/source.d.ts.map +1 -0
  36. package/dist/detectors/source.js +575 -0
  37. package/dist/detectors/source.js.map +1 -0
  38. package/dist/index.d.ts +31 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +39 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/inventory.d.ts +15 -0
  43. package/dist/inventory.d.ts.map +1 -0
  44. package/dist/inventory.js +74 -0
  45. package/dist/inventory.js.map +1 -0
  46. package/dist/parallel.d.ts +34 -0
  47. package/dist/parallel.d.ts.map +1 -0
  48. package/dist/parallel.js +237 -0
  49. package/dist/parallel.js.map +1 -0
  50. package/dist/registry.d.ts +42 -0
  51. package/dist/registry.d.ts.map +1 -0
  52. package/dist/registry.js +51 -0
  53. package/dist/registry.js.map +1 -0
  54. package/dist/remediation.d.ts +42 -0
  55. package/dist/remediation.d.ts.map +1 -0
  56. package/dist/remediation.js +131 -0
  57. package/dist/remediation.js.map +1 -0
  58. package/dist/report.d.ts +24 -0
  59. package/dist/report.d.ts.map +1 -0
  60. package/dist/report.js +286 -0
  61. package/dist/report.js.map +1 -0
  62. package/dist/scan-worker.d.ts +2 -0
  63. package/dist/scan-worker.d.ts.map +1 -0
  64. package/dist/scan-worker.js +63 -0
  65. package/dist/scan-worker.js.map +1 -0
  66. package/dist/scan.d.ts +27 -0
  67. package/dist/scan.d.ts.map +1 -0
  68. package/dist/scan.js +168 -0
  69. package/dist/scan.js.map +1 -0
  70. package/dist/types.d.ts +190 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +9 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/version.d.ts +7 -0
  75. package/dist/version.d.ts.map +1 -0
  76. package/dist/version.js +7 -0
  77. package/dist/version.js.map +1 -0
  78. package/dist/walk.d.ts +37 -0
  79. package/dist/walk.d.ts.map +1 -0
  80. package/dist/walk.js +260 -0
  81. package/dist/walk.js.map +1 -0
  82. package/package.json +40 -0
  83. package/src/baseline.ts +111 -0
  84. package/src/cbom.ts +166 -0
  85. package/src/changed.ts +76 -0
  86. package/src/config.ts +295 -0
  87. package/src/cwe.ts +20 -0
  88. package/src/dependencies.ts +299 -0
  89. package/src/detect-utils.ts +157 -0
  90. package/src/detectors/pem.ts +162 -0
  91. package/src/detectors/source.ts +733 -0
  92. package/src/index.ts +79 -0
  93. package/src/inventory.ts +94 -0
  94. package/src/parallel.ts +288 -0
  95. package/src/registry.ts +71 -0
  96. package/src/remediation.ts +173 -0
  97. package/src/report.ts +340 -0
  98. package/src/scan-worker.ts +80 -0
  99. package/src/scan.ts +187 -0
  100. package/src/types.ts +224 -0
  101. package/src/version.ts +6 -0
  102. package/src/walk.ts +289 -0
@@ -0,0 +1,733 @@
1
+ /**
2
+ * Source-code detectors for classical, non-quantum-safe asymmetric cryptography
3
+ * in JavaScript / TypeScript. Each detector is pure and stateless: it declares
4
+ * which files it applies to and returns zero or more Findings for a file's
5
+ * contents.
6
+ *
7
+ * The detection strategy is deliberately lexical (regex over source text). This
8
+ * is robust to bundling and partial files and keeps the package dependency-free.
9
+ * Confidence is set per-pattern to reflect how specific the match is.
10
+ *
11
+ * All per-file regexes are precompiled at module scope (not re-created per
12
+ * file) — `eachMatch` clones a fresh stateful copy only when a regex lacks the
13
+ * global flag, and these are all global, so they are reused safely.
14
+ *
15
+ * HNDL (harvest-now-decrypt-later) policy:
16
+ * - confidentiality primitives (key exchange / KEM: ECDH, DH, RSA-OAEP) → hndl:true
17
+ * - signatures (RSA-PSS, ECDSA, EdDSA, DSA, JWT alg) → hndl:false, but still high
18
+ * severity because a quantum attacker can forge them.
19
+ * - EC keygen is ambiguous (an 'ec' key feeds BOTH ECDSA and ECDH); it is
20
+ * classified conservatively as key-exchange-capable (hndl:true).
21
+ */
22
+ import type { Detector, Finding } from "../types.js";
23
+ import {
24
+ JS_TS_EXTENSIONS,
25
+ eachMatch,
26
+ hasExtension,
27
+ makeFinding,
28
+ nearSortedCall,
29
+ } from "../detect-utils.js";
30
+ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
31
+
32
+ /* -------------------------------------------------------------------------- */
33
+ /* Precompiled regexes (module scope — never recreated per file) */
34
+ /* -------------------------------------------------------------------------- */
35
+
36
+ const RE_GENERATE_KEYPAIR =
37
+ /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
38
+ const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
39
+ // One-shot crypto.sign/verify(algorithm, data, key). Anchored so it doesn't
40
+ // fire inside identifiers like `assign(` or `createSign(` (which the dedicated
41
+ // createSign/createVerify rule handles).
42
+ const RE_ONESHOT_SIGN_VERIFY =
43
+ /(?:^|[^.\w])(?:crypto\.)?(sign|verify)\s*\(\s*['"`][\w.-]+['"`]\s*,/g;
44
+ const RE_CREATE_DH = /createDiffieHellman(?:Group)?\s*\(/g;
45
+ const RE_GET_DH = /getDiffieHellman\s*\(\s*['"`](modp\d+)['"`]\s*\)/g;
46
+ const RE_CREATE_ECDH = /createECDH\s*\(/g;
47
+ const RE_RSA_ENCRYPT = /(?:crypto\.)?(?:publicEncrypt|privateDecrypt)\s*\(/g;
48
+ const RE_DH_KEYOBJECT = /(?:crypto\.)?diffieHellman\s*\(\s*\{/g;
49
+
50
+ // WebCrypto.
51
+ const RE_WEBCRYPTO_ALGO = /\b(RSA-OAEP|RSA-PSS|RSASSA-PKCS1-v1_5|ECDH|ECDSA)\b/gi;
52
+ const RE_SUBTLE_CALL =
53
+ /subtle\s*\.\s*(generateKey|importKey|exportKey|deriveKey|deriveBits|sign|verify|encrypt|decrypt|wrapKey|unwrapKey)\s*\(/g;
54
+
55
+ // Libraries.
56
+ const RE_FORGE_RSA = /pki\.rsa\.generateKeyPair\s*\(/g;
57
+ const RE_FORGE_ED25519 = /forge\.ed25519\b/g;
58
+ const RE_ELLIPTIC_EC = /new\s+(?:elliptic\.)?ec\s*\(/gi;
59
+ const RE_JSRSASIGN_KEYGEN = /KEYUTIL\.generateKeypair\s*\(/g;
60
+ const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
61
+ const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
62
+ // secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
63
+ const RE_SECP256K1 =
64
+ /\b(?:secp(?:256k1)?|secp)\s*\.\s*(?:sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
65
+
66
+ // JWT/JOSE.
67
+ const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
68
+ // JOSE ECDH-ES key agreement (HNDL) and COSE algorithm identifiers.
69
+ const RE_JOSE_ECDH = /['"`](ECDH-ES(?:\+A(?:128|192|256)KW)?)['"`]/g;
70
+
71
+ // TLS config.
72
+ const RE_TLS_LEGACY_VERSION =
73
+ /(?:minVersion|maxVersion)\s*:\s*['"`]TLSv1(?:\.1)?['"`]|secureProtocol\s*:\s*['"`]TLSv1(?:_1)?_method['"`]/g;
74
+ const RE_TLS_REJECT = /rejectUnauthorized\s*:\s*false/g;
75
+ // Hardened cipher regex: bounded spans (no unbounded `[^'"`]*` straddling the
76
+ // alternation), single-quote-style anchoring removed in favour of {0,256} bounds
77
+ // so worst-case backtracking is linear in the bound, not the file (P0-6).
78
+ const RE_TLS_WEAK_CIPHER =
79
+ /ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
80
+
81
+ /* -------------------------------------------------------------------------- */
82
+ /* Node.js `crypto` module */
83
+ /* -------------------------------------------------------------------------- */
84
+
85
+ /** Detects classical asymmetric usage from Node's built-in `crypto` module. */
86
+ const nodeCryptoDetector: Detector = {
87
+ id: "node-crypto",
88
+ description: "Classical asymmetric crypto via the Node.js `crypto` module",
89
+ scope: "source",
90
+ language: "js",
91
+ appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
92
+ detect({ file, content }): Finding[] {
93
+ const findings: Finding[] = [];
94
+ const push = (
95
+ spec: Omit<Parameters<typeof makeFinding>[0], "file" | "content" | "index" | "matchLength">,
96
+ m: RegExpExecArray,
97
+ ) =>
98
+ findings.push(
99
+ makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length }),
100
+ );
101
+
102
+ // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
103
+ eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
104
+ const type = m[1].toLowerCase();
105
+ const map: Record<
106
+ string,
107
+ {
108
+ algo: Finding["algorithm"];
109
+ cat: Finding["category"];
110
+ sev: Finding["severity"];
111
+ hndl: boolean;
112
+ label: string;
113
+ message?: string;
114
+ remediation?: string;
115
+ }
116
+ > = {
117
+ rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
118
+ // EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
119
+ // HNDL-exposed, so classify conservatively as key-exchange-capable and
120
+ // surface both concerns rather than asserting signature-only (P0-4).
121
+ ec: {
122
+ algo: "ECDH",
123
+ cat: "key-exchange",
124
+ sev: "high",
125
+ hndl: true,
126
+ label: "EC (ECDSA/ECDH)",
127
+ message:
128
+ "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
129
+ "and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
130
+ remediation:
131
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
132
+ },
133
+ dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
134
+ dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
135
+ x25519: { algo: "X25519", cat: "key-exchange", sev: "low", hndl: true, label: "X25519" },
136
+ x448: { algo: "X448", cat: "key-exchange", sev: "low", hndl: true, label: "X448" },
137
+ ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
138
+ ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
139
+ };
140
+ const info = map[type];
141
+ push(
142
+ {
143
+ ruleId: "node-crypto-keygen",
144
+ title: `${info.label} key generation`,
145
+ category: info.cat,
146
+ severity: info.sev,
147
+ confidence: "high",
148
+ algorithm: info.algo,
149
+ hndl: info.hndl,
150
+ cwe: CWE_BROKEN_CRYPTO,
151
+ message:
152
+ info.message ??
153
+ `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
154
+ ...(info.remediation ? { remediation: info.remediation } : {}),
155
+ },
156
+ m,
157
+ );
158
+ });
159
+
160
+ // createSign / createVerify — RSA / ECDSA / DSA signatures.
161
+ eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
162
+ push(
163
+ {
164
+ ruleId: "node-crypto-sign",
165
+ title: "Classical signature (createSign/createVerify)",
166
+ category: "signature",
167
+ severity: "high",
168
+ confidence: "medium",
169
+ algorithm: "unknown",
170
+ hndl: false,
171
+ cwe: CWE_BROKEN_CRYPTO,
172
+ message:
173
+ "Uses createSign/createVerify, typically RSA, ECDSA or DSA — all forgeable by a quantum attacker.",
174
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
175
+ },
176
+ m,
177
+ );
178
+ });
179
+
180
+ // One-shot crypto.sign(algorithm, data, key) / crypto.verify(...) (Node ≥ 12).
181
+ eachMatch(RE_ONESHOT_SIGN_VERIFY, content, (m) => {
182
+ push(
183
+ {
184
+ ruleId: "node-crypto-sign-oneshot",
185
+ title: "Classical one-shot signature (crypto.sign/verify)",
186
+ category: "signature",
187
+ severity: "high",
188
+ confidence: "medium",
189
+ algorithm: "unknown",
190
+ hndl: false,
191
+ cwe: CWE_BROKEN_CRYPTO,
192
+ message:
193
+ "Uses the one-shot crypto.sign/crypto.verify API, typically RSA/ECDSA/EdDSA — forgeable by a quantum attacker.",
194
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
195
+ },
196
+ m,
197
+ );
198
+ });
199
+
200
+ // createDiffieHellman / createDiffieHellmanGroup — finite-field DH key exchange.
201
+ eachMatch(RE_CREATE_DH, content, (m) => {
202
+ push(
203
+ {
204
+ ruleId: "node-crypto-dh",
205
+ title: "Diffie-Hellman key exchange",
206
+ category: "key-exchange",
207
+ severity: "high",
208
+ confidence: "high",
209
+ algorithm: "DH",
210
+ hndl: true,
211
+ cwe: CWE_BROKEN_CRYPTO,
212
+ message:
213
+ "Finite-field Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
214
+ },
215
+ m,
216
+ );
217
+ });
218
+
219
+ // getDiffieHellman('modpN') — named built-in finite-field MODP groups.
220
+ eachMatch(RE_GET_DH, content, (m) => {
221
+ push(
222
+ {
223
+ ruleId: "node-crypto-dh-modp",
224
+ title: `Diffie-Hellman MODP group (${m[1]})`,
225
+ category: "key-exchange",
226
+ severity: "high",
227
+ confidence: "high",
228
+ algorithm: "DH",
229
+ hndl: true,
230
+ cwe: CWE_BROKEN_CRYPTO,
231
+ message: `Named finite-field DH MODP group "${m[1]}" is broken by Shor's algorithm (harvest-now-decrypt-later).`,
232
+ },
233
+ m,
234
+ );
235
+ });
236
+
237
+ // createECDH — elliptic-curve Diffie-Hellman key exchange.
238
+ eachMatch(RE_CREATE_ECDH, content, (m) => {
239
+ push(
240
+ {
241
+ ruleId: "node-crypto-ecdh",
242
+ title: "ECDH key exchange",
243
+ category: "key-exchange",
244
+ severity: "high",
245
+ confidence: "high",
246
+ algorithm: "ECDH",
247
+ hndl: true,
248
+ cwe: CWE_BROKEN_CRYPTO,
249
+ message:
250
+ "Elliptic-curve Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
251
+ },
252
+ m,
253
+ );
254
+ });
255
+
256
+ // publicEncrypt / privateDecrypt — RSA encryption (KEM-like confidentiality).
257
+ eachMatch(RE_RSA_ENCRYPT, content, (m) => {
258
+ push(
259
+ {
260
+ ruleId: "node-crypto-rsa-encrypt",
261
+ title: "RSA public-key encryption",
262
+ category: "kem",
263
+ severity: "high",
264
+ confidence: "high",
265
+ algorithm: "RSA",
266
+ hndl: true,
267
+ cwe: CWE_BROKEN_CRYPTO,
268
+ message:
269
+ "RSA public-key encryption is broken by Shor's algorithm and exposed to harvest-now-decrypt-later.",
270
+ },
271
+ m,
272
+ );
273
+ });
274
+
275
+ // diffieHellman({ privateKey, publicKey }) — KeyObject-based DH/ECDH.
276
+ eachMatch(RE_DH_KEYOBJECT, content, (m) => {
277
+ push(
278
+ {
279
+ ruleId: "node-crypto-dh-keyobject",
280
+ title: "Diffie-Hellman (KeyObject) key exchange",
281
+ category: "key-exchange",
282
+ severity: "high",
283
+ confidence: "high",
284
+ algorithm: "ECDH",
285
+ hndl: true,
286
+ cwe: CWE_BROKEN_CRYPTO,
287
+ message:
288
+ "crypto.diffieHellman() performs a classical (EC)DH agreement (harvest-now-decrypt-later).",
289
+ },
290
+ m,
291
+ );
292
+ });
293
+
294
+ return findings;
295
+ },
296
+ };
297
+
298
+ /* -------------------------------------------------------------------------- */
299
+ /* WebCrypto (SubtleCrypto) */
300
+ /* -------------------------------------------------------------------------- */
301
+
302
+ /**
303
+ * Detects classical algorithms passed to WebCrypto's SubtleCrypto methods. The
304
+ * algorithm name can appear as a bare string ("RSA-OAEP") or as
305
+ * `{ name: "ECDH" }`; we scan both forms within a window after a subtle call.
306
+ */
307
+ const webCryptoDetector: Detector = {
308
+ id: "webcrypto",
309
+ description: "Classical asymmetric algorithms via WebCrypto SubtleCrypto",
310
+ scope: "source",
311
+ language: "js",
312
+ appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
313
+ detect({ file, content }): Finding[] {
314
+ const findings: Finding[] = [];
315
+
316
+ // Only consider names that appear near a subtle.* call to reduce noise.
317
+ // callIndexes is collected in ascending order (regex scans left→right), so
318
+ // proximity is resolved with a binary search instead of an O(M·C) scan.
319
+ const callIndexes: number[] = [];
320
+ eachMatch(RE_SUBTLE_CALL, content, (m) => callIndexes.push(m.index));
321
+ if (callIndexes.length === 0) return findings;
322
+
323
+ eachMatch(RE_WEBCRYPTO_ALGO, content, (m) => {
324
+ if (!nearSortedCall(callIndexes, m.index, 400)) return;
325
+ const name = m[1].toUpperCase();
326
+ const isKem = name === "RSA-OAEP";
327
+ const isEcdh = name === "ECDH";
328
+ const algorithm: Finding["algorithm"] = name.startsWith("RSA")
329
+ ? "RSA"
330
+ : isEcdh
331
+ ? "ECDH"
332
+ : "ECDSA";
333
+ const category: Finding["category"] = isEcdh ? "key-exchange" : isKem ? "kem" : "signature";
334
+ const hndl = isKem || isEcdh;
335
+ findings.push(
336
+ makeFinding({
337
+ ruleId: "webcrypto-classical",
338
+ title: `WebCrypto ${m[1]}`,
339
+ category,
340
+ severity: "high",
341
+ confidence: "high",
342
+ algorithm,
343
+ hndl,
344
+ cwe: CWE_BROKEN_CRYPTO,
345
+ message: `WebCrypto algorithm "${m[1]}" is classical asymmetric crypto and not quantum-safe.`,
346
+ file,
347
+ content,
348
+ index: m.index,
349
+ matchLength: m[0].length,
350
+ }),
351
+ );
352
+ });
353
+
354
+ return findings;
355
+ },
356
+ };
357
+
358
+ /* -------------------------------------------------------------------------- */
359
+ /* Popular crypto libraries */
360
+ /* -------------------------------------------------------------------------- */
361
+
362
+ /** Detects classical crypto from popular npm libraries used in source. */
363
+ const libraryDetector: Detector = {
364
+ id: "crypto-libs",
365
+ description: "Classical asymmetric crypto via node-forge, elliptic, jsrsasign, node-rsa",
366
+ scope: "source",
367
+ language: "js",
368
+ appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
369
+ detect({ file, content }): Finding[] {
370
+ const findings: Finding[] = [];
371
+ const add = (
372
+ re: RegExp,
373
+ spec: Omit<Parameters<typeof makeFinding>[0], "file" | "content" | "index" | "matchLength">,
374
+ ) =>
375
+ eachMatch(re, content, (m) =>
376
+ findings.push(
377
+ makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length }),
378
+ ),
379
+ );
380
+
381
+ // node-forge: pki.rsa.generateKeyPair(...)
382
+ add(RE_FORGE_RSA, {
383
+ ruleId: "forge-rsa-keygen",
384
+ title: "node-forge RSA key generation",
385
+ category: "kem",
386
+ severity: "high",
387
+ confidence: "high",
388
+ algorithm: "RSA",
389
+ hndl: true,
390
+ cwe: CWE_BROKEN_CRYPTO,
391
+ message: "node-forge generates a classical RSA key pair, which is not quantum-safe.",
392
+ });
393
+
394
+ // node-forge: forge.ed25519.* (classical EdDSA)
395
+ add(RE_FORGE_ED25519, {
396
+ ruleId: "forge-ed25519",
397
+ title: "node-forge Ed25519 usage",
398
+ category: "signature",
399
+ severity: "low",
400
+ confidence: "high",
401
+ algorithm: "EdDSA",
402
+ hndl: false,
403
+ cwe: CWE_BROKEN_CRYPTO,
404
+ message: "node-forge Ed25519 is a modern but still classical signature scheme.",
405
+ });
406
+
407
+ // elliptic: new EC('secp256k1') / new ec(...)
408
+ add(RE_ELLIPTIC_EC, {
409
+ ruleId: "elliptic-ec",
410
+ title: "elliptic curve instantiation",
411
+ category: "signature",
412
+ severity: "high",
413
+ confidence: "high",
414
+ algorithm: "ECDSA",
415
+ hndl: false,
416
+ cwe: CWE_BROKEN_CRYPTO,
417
+ message:
418
+ "The `elliptic` library implements classical ECDSA/ECDH, both broken by Shor's algorithm.",
419
+ });
420
+
421
+ // Direct secp256k1 API usage: secp.sign / getPublicKey / getSharedSecret.
422
+ add(RE_SECP256K1, {
423
+ ruleId: "secp256k1-usage",
424
+ title: "secp256k1 ECDSA/ECDH usage",
425
+ category: "signature",
426
+ severity: "high",
427
+ confidence: "medium",
428
+ algorithm: "ECDSA",
429
+ hndl: false,
430
+ cwe: CWE_BROKEN_CRYPTO,
431
+ message:
432
+ "Direct secp256k1 usage (ECDSA signatures / ECDH agreement) is classical and broken by Shor's algorithm.",
433
+ remediation: "ML-DSA-65 (FIPS 204) for signatures; hybrid X25519MLKEM768 for key agreement.",
434
+ });
435
+
436
+ // jsrsasign: KEYUTIL.generateKeypair('RSA'|'EC', ...) or KJUR.crypto.*
437
+ add(RE_JSRSASIGN_KEYGEN, {
438
+ ruleId: "jsrsasign-keygen",
439
+ title: "jsrsasign key generation",
440
+ category: "signature",
441
+ severity: "high",
442
+ confidence: "high",
443
+ algorithm: "unknown",
444
+ hndl: false,
445
+ cwe: CWE_BROKEN_CRYPTO,
446
+ message: "jsrsasign generates classical RSA/EC key pairs, which are not quantum-safe.",
447
+ remediation: "ML-KEM-768 (FIPS 203) / ML-DSA-65 (FIPS 204)",
448
+ });
449
+ add(RE_JSRSASIGN_SIGN, {
450
+ ruleId: "jsrsasign-sign",
451
+ title: "jsrsasign signature",
452
+ category: "signature",
453
+ severity: "high",
454
+ confidence: "high",
455
+ algorithm: "unknown",
456
+ hndl: false,
457
+ cwe: CWE_BROKEN_CRYPTO,
458
+ message:
459
+ "jsrsasign signing uses classical RSA/ECDSA signatures, forgeable by a quantum attacker.",
460
+ remediation: "ML-DSA-65 (FIPS 204)",
461
+ });
462
+
463
+ // node-rsa: new NodeRSA(...)
464
+ add(RE_NODE_RSA, {
465
+ ruleId: "node-rsa",
466
+ title: "node-rsa key/usage",
467
+ category: "kem",
468
+ severity: "high",
469
+ confidence: "high",
470
+ algorithm: "RSA",
471
+ hndl: true,
472
+ cwe: CWE_BROKEN_CRYPTO,
473
+ message: "node-rsa wraps classical RSA encryption/signing, which is not quantum-safe.",
474
+ });
475
+
476
+ return findings;
477
+ },
478
+ };
479
+
480
+ /* -------------------------------------------------------------------------- */
481
+ /* JWT / JOSE / COSE algorithm strings */
482
+ /* -------------------------------------------------------------------------- */
483
+
484
+ /**
485
+ * Detects classical signature algorithm identifiers used by JWT/JOSE, plus
486
+ * ECDH-ES key-agreement identifiers (HNDL-exposed). These appear as string
487
+ * literals: `alg: "RS256"`, `algorithms: ["ES256"]`, `enc: "ECDH-ES+A256KW"`.
488
+ */
489
+ const jwtDetector: Detector = {
490
+ id: "jwt-jose",
491
+ description: "Classical JWT/JOSE algorithms (RS/PS/ES/EdDSA) and ECDH-ES key agreement",
492
+ scope: "source",
493
+ language: "js",
494
+ appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
495
+ detect({ file, content }): Finding[] {
496
+ const findings: Finding[] = [];
497
+
498
+ // Classical JWS signature alg tokens. Anchored to quotes to avoid words.
499
+ eachMatch(RE_JWT_ALG, content, (m) => {
500
+ const alg = m[1];
501
+ let algorithm: Finding["algorithm"];
502
+ if (alg.startsWith("RS") || alg.startsWith("PS")) algorithm = "RSA";
503
+ else if (alg === "EdDSA") algorithm = "EdDSA";
504
+ else algorithm = "ECDSA"; // ES*
505
+ findings.push(
506
+ makeFinding({
507
+ ruleId: "jwt-classical-alg",
508
+ title: `JWT/JOSE algorithm ${alg}`,
509
+ category: "signature",
510
+ severity: "high",
511
+ confidence: "medium",
512
+ algorithm,
513
+ hndl: false,
514
+ cwe: CWE_BROKEN_CRYPTO,
515
+ message: `JWT/JOSE algorithm "${alg}" is a classical signature, forgeable by a quantum attacker.`,
516
+ remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
517
+ file,
518
+ content,
519
+ index: m.index,
520
+ matchLength: m[0].length,
521
+ }),
522
+ );
523
+ });
524
+
525
+ // JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
526
+ eachMatch(RE_JOSE_ECDH, content, (m) => {
527
+ findings.push(
528
+ makeFinding({
529
+ ruleId: "jose-ecdh-es",
530
+ title: `JOSE key agreement ${m[1]}`,
531
+ category: "key-exchange",
532
+ severity: "high",
533
+ confidence: "medium",
534
+ algorithm: "ECDH",
535
+ hndl: true,
536
+ cwe: CWE_BROKEN_CRYPTO,
537
+ message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
538
+ remediation:
539
+ "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
540
+ file,
541
+ content,
542
+ index: m.index,
543
+ matchLength: m[0].length,
544
+ }),
545
+ );
546
+ });
547
+
548
+ return findings;
549
+ },
550
+ };
551
+
552
+ /* -------------------------------------------------------------------------- */
553
+ /* TLS legacy configuration */
554
+ /* -------------------------------------------------------------------------- */
555
+
556
+ /**
557
+ * Detects legacy / insecure TLS configuration expressed as JS object literals:
558
+ * forced TLS 1.0/1.1, disabled certificate verification, and weak ciphers.
559
+ * These aren't quantum-specific but materially weaken transport security and
560
+ * are squarely in qScan's "config" scope.
561
+ */
562
+ const tlsDetector: Detector = {
563
+ id: "tls-config",
564
+ description: "Legacy / insecure TLS configuration in JS objects",
565
+ scope: "config",
566
+ language: "js",
567
+ appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
568
+ detect({ file, content }): Finding[] {
569
+ const findings: Finding[] = [];
570
+
571
+ // minVersion / maxVersion / secureProtocol pinned to TLS 1.0 or 1.1.
572
+ eachMatch(RE_TLS_LEGACY_VERSION, content, (m) => {
573
+ findings.push(
574
+ makeFinding({
575
+ ruleId: "tls-legacy-version",
576
+ title: "Legacy TLS version pinned",
577
+ category: "tls",
578
+ severity: "medium",
579
+ confidence: "high",
580
+ hndl: false,
581
+ cwe: CWE_WEAK_STRENGTH,
582
+ message: "TLS 1.0/1.1 are deprecated and insecure; require TLS 1.3.",
583
+ remediation: "Set minVersion: 'TLSv1.3' and prefer PQC-hybrid key exchange.",
584
+ file,
585
+ content,
586
+ index: m.index,
587
+ matchLength: m[0].length,
588
+ }),
589
+ );
590
+ });
591
+
592
+ // rejectUnauthorized: false — disables certificate verification.
593
+ eachMatch(RE_TLS_REJECT, content, (m) => {
594
+ findings.push(
595
+ makeFinding({
596
+ ruleId: "tls-reject-unauthorized",
597
+ title: "TLS certificate verification disabled",
598
+ category: "tls",
599
+ severity: "high",
600
+ confidence: "high",
601
+ hndl: false,
602
+ cwe: CWE_CERT_VALIDATION,
603
+ message: "rejectUnauthorized:false disables TLS certificate verification (MITM risk).",
604
+ remediation: "Remove rejectUnauthorized:false; verify certificates properly.",
605
+ file,
606
+ content,
607
+ index: m.index,
608
+ matchLength: m[0].length,
609
+ }),
610
+ );
611
+ });
612
+
613
+ // Weak / export ciphers referenced in a ciphers string (bounded regex).
614
+ eachMatch(RE_TLS_WEAK_CIPHER, content, (m) => {
615
+ findings.push(
616
+ makeFinding({
617
+ ruleId: "tls-weak-cipher",
618
+ title: "Weak TLS cipher configured",
619
+ category: "tls",
620
+ severity: "medium",
621
+ confidence: "medium",
622
+ hndl: false,
623
+ cwe: CWE_WEAK_STRENGTH,
624
+ message: `Weak cipher (${m[1]}) configured in the TLS ciphers list.`,
625
+ remediation: "Use a modern AEAD cipher suite (TLS 1.3 defaults).",
626
+ file,
627
+ content,
628
+ index: m.index,
629
+ matchLength: m[0].length,
630
+ }),
631
+ );
632
+ });
633
+
634
+ return findings;
635
+ },
636
+ };
637
+
638
+ /* -------------------------------------------------------------------------- */
639
+ /* SSH public keys + TLS certificate signature algorithms (config scope) */
640
+ /* -------------------------------------------------------------------------- */
641
+
642
+ const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
643
+ const RE_CERT_SIG_ALG =
644
+ /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA(?:1|256|384|512)|rsassaPss|dsaWithSHA(?:1|256))\b/g;
645
+
646
+ /**
647
+ * Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
648
+ * and X.509 certificate signature-algorithm identifiers in any text file. These
649
+ * are language-agnostic config surfaces — the SSH-key forgery surface and the
650
+ * PKI signature surface that lexical PEM detection misses.
651
+ */
652
+ const sshCertDetector: Detector = {
653
+ id: "ssh-cert",
654
+ description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
655
+ scope: "config",
656
+ language: "any",
657
+ appliesTo: () => true,
658
+ detect({ file, content }): Finding[] {
659
+ const findings: Finding[] = [];
660
+
661
+ // SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
662
+ eachMatch(RE_SSH_PUBKEY, content, (m) => {
663
+ const tok = m[1];
664
+ const algorithm: Finding["algorithm"] = tok.startsWith("ssh-rsa")
665
+ ? "RSA"
666
+ : tok === "ssh-ed25519"
667
+ ? "EdDSA"
668
+ : tok === "ssh-dss"
669
+ ? "DSA"
670
+ : "ECDSA";
671
+ findings.push(
672
+ makeFinding({
673
+ ruleId: "ssh-public-key",
674
+ title: `Classical SSH public key (${tok})`,
675
+ category: "certificate",
676
+ severity: "low",
677
+ confidence: "medium",
678
+ algorithm,
679
+ hndl: false,
680
+ cwe: CWE_BROKEN_CRYPTO,
681
+ message: `SSH public key type "${tok}" is a classical key forgeable by a quantum attacker.`,
682
+ remediation:
683
+ "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
684
+ file,
685
+ content,
686
+ index: m.index,
687
+ matchLength: m[0].length,
688
+ }),
689
+ );
690
+ });
691
+
692
+ // X.509 / TLS certificate signature algorithm identifiers (forgery surface).
693
+ eachMatch(RE_CERT_SIG_ALG, content, (m) => {
694
+ const tok = m[1];
695
+ const algorithm: Finding["algorithm"] = /RSA|rsassa/i.test(tok)
696
+ ? "RSA"
697
+ : tok.startsWith("ecdsa")
698
+ ? "ECDSA"
699
+ : "DSA";
700
+ findings.push(
701
+ makeFinding({
702
+ ruleId: "cert-signature-algorithm",
703
+ title: `Classical certificate signature algorithm (${tok})`,
704
+ category: "certificate",
705
+ severity: "low",
706
+ confidence: "medium",
707
+ algorithm,
708
+ hndl: false,
709
+ cwe: CWE_BROKEN_CRYPTO,
710
+ message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
711
+ remediation:
712
+ "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
713
+ file,
714
+ content,
715
+ index: m.index,
716
+ matchLength: m[0].length,
717
+ }),
718
+ );
719
+ });
720
+
721
+ return findings;
722
+ },
723
+ };
724
+
725
+ /** All built-in source/config detectors, in run order. */
726
+ export const sourceDetectors: Detector[] = [
727
+ nodeCryptoDetector,
728
+ webCryptoDetector,
729
+ libraryDetector,
730
+ jwtDetector,
731
+ tlsDetector,
732
+ sshCertDetector,
733
+ ];