@quantakrypto/core 0.2.1 → 0.3.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 (58) hide show
  1. package/dist/dependencies.d.ts.map +1 -1
  2. package/dist/dependencies.js +105 -0
  3. package/dist/dependencies.js.map +1 -1
  4. package/dist/detect-utils.d.ts +26 -1
  5. package/dist/detect-utils.d.ts.map +1 -1
  6. package/dist/detect-utils.js +26 -0
  7. package/dist/detect-utils.js.map +1 -1
  8. package/dist/detectors/pem.d.ts +4 -0
  9. package/dist/detectors/pem.d.ts.map +1 -1
  10. package/dist/detectors/pem.js +113 -91
  11. package/dist/detectors/pem.js.map +1 -1
  12. package/dist/detectors/source.d.ts +9 -2
  13. package/dist/detectors/source.d.ts.map +1 -1
  14. package/dist/detectors/source.js +366 -265
  15. package/dist/detectors/source.js.map +1 -1
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/inventory.d.ts +6 -3
  21. package/dist/inventory.d.ts.map +1 -1
  22. package/dist/inventory.js +11 -6
  23. package/dist/inventory.js.map +1 -1
  24. package/dist/parallel.d.ts.map +1 -1
  25. package/dist/parallel.js +2 -0
  26. package/dist/parallel.js.map +1 -1
  27. package/dist/registry.d.ts +16 -1
  28. package/dist/registry.d.ts.map +1 -1
  29. package/dist/registry.js +31 -0
  30. package/dist/registry.js.map +1 -1
  31. package/dist/report.d.ts +9 -1
  32. package/dist/report.d.ts.map +1 -1
  33. package/dist/report.js +56 -23
  34. package/dist/report.js.map +1 -1
  35. package/dist/scan-worker.js +1 -1
  36. package/dist/scan-worker.js.map +1 -1
  37. package/dist/scan.d.ts +1 -1
  38. package/dist/scan.d.ts.map +1 -1
  39. package/dist/scan.js +8 -2
  40. package/dist/scan.js.map +1 -1
  41. package/dist/types.d.ts +53 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.js +1 -1
  45. package/package.json +1 -1
  46. package/src/dependencies.ts +105 -0
  47. package/src/detect-utils.ts +57 -1
  48. package/src/detectors/pem.ts +124 -105
  49. package/src/detectors/source.ts +466 -343
  50. package/src/index.ts +2 -1
  51. package/src/inventory.ts +12 -6
  52. package/src/parallel.ts +9 -1
  53. package/src/registry.ts +39 -1
  54. package/src/report.ts +79 -23
  55. package/src/scan-worker.ts +12 -5
  56. package/src/scan.ts +19 -5
  57. package/src/types.ts +54 -0
  58. package/src/version.ts +1 -1
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * Source-code detectors for classical, non-quantum-safe asymmetric cryptography
3
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.
4
+ * which files it applies to, the catalog of rules it can emit (`rules`), and
5
+ * returns zero or more Findings for a file's contents.
6
+ *
7
+ * Rule metadata (title / severity / category / remediation / …) lives ONCE in
8
+ * the per-detector `RuleMeta` declarations below, not inline in `detect()`.
9
+ * `detect()` builds findings from those declarations via `findingFromRule`,
10
+ * overriding only the fields that genuinely vary per match (e.g. the concrete
11
+ * algorithm family of a `generateKeyPair('ec')` call). The declarations are the
12
+ * catalog surfaced by the registry, SARIF `rules[]`, and the MCP resolver.
6
13
  *
7
14
  * The detection strategy is deliberately lexical (regex over source text). This
8
15
  * is robust to bundling and partial files and keeps the package dependency-free.
@@ -19,12 +26,12 @@
19
26
  * - EC keygen is ambiguous (an 'ec' key feeds BOTH ECDSA and ECDH); it is
20
27
  * classified conservatively as key-exchange-capable (hndl:true).
21
28
  */
22
- import type { Detector, Finding } from "../types.js";
29
+ import type { Detector, Finding, RuleMeta } from "../types.js";
23
30
  import {
24
31
  JS_TS_EXTENSIONS,
25
32
  eachMatch,
33
+ findingFromRule,
26
34
  hasExtension,
27
- makeFinding,
28
35
  nearSortedCall,
29
36
  } from "../detect-utils.js";
30
37
  import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
@@ -84,22 +91,135 @@ const RE_TLS_WEAK_CIPHER =
84
91
  /* Node.js `crypto` module */
85
92
  /* -------------------------------------------------------------------------- */
86
93
 
94
+ /**
95
+ * Rule catalog for the Node `crypto` detector. `node-crypto-keygen` and
96
+ * `node-crypto-dh-modp` refine some fields per match; the rest emit findings
97
+ * straight from these declarations.
98
+ */
99
+ const RULE_NODE_KEYGEN: RuleMeta = {
100
+ id: "node-crypto-keygen",
101
+ title: "Classical key generation",
102
+ description: "crypto.generateKeyPair(Sync)('rsa'|'ec'|'dsa'|'dh'|…)",
103
+ category: "key-exchange",
104
+ severity: "high",
105
+ confidence: "high",
106
+ algorithm: "unknown",
107
+ hndl: true,
108
+ cwe: CWE_BROKEN_CRYPTO,
109
+ message: "Generates a classical asymmetric key pair, which is not quantum-safe.",
110
+ };
111
+ const RULE_NODE_SIGN: RuleMeta = {
112
+ id: "node-crypto-sign",
113
+ title: "Classical signature (createSign/createVerify)",
114
+ description: "crypto.createSign / crypto.createVerify",
115
+ category: "signature",
116
+ severity: "high",
117
+ confidence: "medium",
118
+ algorithm: "unknown",
119
+ hndl: false,
120
+ cwe: CWE_BROKEN_CRYPTO,
121
+ message:
122
+ "Uses createSign/createVerify, typically RSA, ECDSA or DSA — all forgeable by a quantum attacker.",
123
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
124
+ };
125
+ const RULE_NODE_SIGN_ONESHOT: RuleMeta = {
126
+ id: "node-crypto-sign-oneshot",
127
+ title: "Classical one-shot signature (crypto.sign/verify)",
128
+ description: "one-shot crypto.sign / crypto.verify (Node ≥ 12)",
129
+ category: "signature",
130
+ severity: "high",
131
+ confidence: "medium",
132
+ algorithm: "unknown",
133
+ hndl: false,
134
+ cwe: CWE_BROKEN_CRYPTO,
135
+ message:
136
+ "Uses the one-shot crypto.sign/crypto.verify API, typically RSA/ECDSA/EdDSA — forgeable by a quantum attacker.",
137
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
138
+ };
139
+ const RULE_NODE_DH: RuleMeta = {
140
+ id: "node-crypto-dh",
141
+ title: "Diffie-Hellman key exchange",
142
+ description: "crypto.createDiffieHellman(Group)",
143
+ category: "key-exchange",
144
+ severity: "high",
145
+ confidence: "high",
146
+ algorithm: "DH",
147
+ hndl: true,
148
+ cwe: CWE_BROKEN_CRYPTO,
149
+ message: "Finite-field Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
150
+ };
151
+ const RULE_NODE_DH_MODP: RuleMeta = {
152
+ id: "node-crypto-dh-modp",
153
+ title: "Diffie-Hellman MODP group",
154
+ description: "crypto.getDiffieHellman('modpN')",
155
+ category: "key-exchange",
156
+ severity: "high",
157
+ confidence: "high",
158
+ algorithm: "DH",
159
+ hndl: true,
160
+ cwe: CWE_BROKEN_CRYPTO,
161
+ message:
162
+ "Named finite-field DH MODP group is broken by Shor's algorithm (harvest-now-decrypt-later).",
163
+ };
164
+ const RULE_NODE_ECDH: RuleMeta = {
165
+ id: "node-crypto-ecdh",
166
+ title: "ECDH key exchange",
167
+ description: "crypto.createECDH",
168
+ category: "key-exchange",
169
+ severity: "high",
170
+ confidence: "high",
171
+ algorithm: "ECDH",
172
+ hndl: true,
173
+ cwe: CWE_BROKEN_CRYPTO,
174
+ message:
175
+ "Elliptic-curve Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
176
+ };
177
+ const RULE_NODE_RSA_ENCRYPT: RuleMeta = {
178
+ id: "node-crypto-rsa-encrypt",
179
+ title: "RSA public-key encryption",
180
+ description: "crypto.publicEncrypt / crypto.privateDecrypt",
181
+ category: "kem",
182
+ severity: "high",
183
+ confidence: "high",
184
+ algorithm: "RSA",
185
+ hndl: true,
186
+ cwe: CWE_BROKEN_CRYPTO,
187
+ message:
188
+ "RSA public-key encryption is broken by Shor's algorithm and exposed to harvest-now-decrypt-later.",
189
+ };
190
+ const RULE_NODE_DH_KEYOBJECT: RuleMeta = {
191
+ id: "node-crypto-dh-keyobject",
192
+ title: "Diffie-Hellman (KeyObject) key exchange",
193
+ description: "crypto.diffieHellman({ privateKey, publicKey })",
194
+ category: "key-exchange",
195
+ severity: "high",
196
+ confidence: "high",
197
+ algorithm: "ECDH",
198
+ hndl: true,
199
+ cwe: CWE_BROKEN_CRYPTO,
200
+ message:
201
+ "crypto.diffieHellman() performs a classical (EC)DH agreement (harvest-now-decrypt-later).",
202
+ };
203
+
87
204
  /** Detects classical asymmetric usage from Node's built-in `crypto` module. */
88
205
  const nodeCryptoDetector: Detector = {
89
206
  id: "node-crypto",
90
207
  description: "Classical asymmetric crypto via the Node.js `crypto` module",
91
208
  scope: "source",
92
209
  language: "js",
210
+ rules: [
211
+ RULE_NODE_KEYGEN,
212
+ RULE_NODE_SIGN,
213
+ RULE_NODE_SIGN_ONESHOT,
214
+ RULE_NODE_DH,
215
+ RULE_NODE_DH_MODP,
216
+ RULE_NODE_ECDH,
217
+ RULE_NODE_RSA_ENCRYPT,
218
+ RULE_NODE_DH_KEYOBJECT,
219
+ ],
93
220
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
94
221
  detect({ file, content }): Finding[] {
95
222
  const findings: Finding[] = [];
96
- const push = (
97
- spec: Omit<Parameters<typeof makeFinding>[0], "file" | "content" | "index" | "matchLength">,
98
- m: RegExpExecArray,
99
- ) =>
100
- findings.push(
101
- makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length }),
102
- );
103
223
 
104
224
  // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
105
225
  eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
@@ -140,156 +260,103 @@ const nodeCryptoDetector: Detector = {
140
260
  ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
141
261
  };
142
262
  const info = map[type];
143
- push(
144
- {
145
- ruleId: "node-crypto-keygen",
146
- title: `${info.label} key generation`,
147
- category: info.cat,
148
- severity: info.sev,
149
- confidence: "high",
150
- algorithm: info.algo,
151
- hndl: info.hndl,
152
- cwe: CWE_BROKEN_CRYPTO,
153
- message:
154
- info.message ??
155
- `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
156
- ...(info.remediation ? { remediation: info.remediation } : {}),
157
- },
158
- m,
263
+ findings.push(
264
+ findingFromRule(
265
+ RULE_NODE_KEYGEN,
266
+ { file, content, index: m.index, matchLength: m[0].length },
267
+ {
268
+ title: `${info.label} key generation`,
269
+ category: info.cat,
270
+ severity: info.sev,
271
+ algorithm: info.algo,
272
+ hndl: info.hndl,
273
+ message:
274
+ info.message ??
275
+ `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
276
+ ...(info.remediation ? { remediation: info.remediation } : {}),
277
+ },
278
+ ),
159
279
  );
160
280
  });
161
281
 
162
282
  // createSign / createVerify — RSA / ECDSA / DSA signatures.
163
283
  eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
164
- push(
165
- {
166
- ruleId: "node-crypto-sign",
167
- title: "Classical signature (createSign/createVerify)",
168
- category: "signature",
169
- severity: "high",
170
- confidence: "medium",
171
- algorithm: "unknown",
172
- hndl: false,
173
- cwe: CWE_BROKEN_CRYPTO,
174
- message:
175
- "Uses createSign/createVerify, typically RSA, ECDSA or DSA — all forgeable by a quantum attacker.",
176
- remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
177
- },
178
- m,
284
+ findings.push(
285
+ findingFromRule(RULE_NODE_SIGN, {
286
+ file,
287
+ content,
288
+ index: m.index,
289
+ matchLength: m[0].length,
290
+ }),
179
291
  );
180
292
  });
181
293
 
182
294
  // One-shot crypto.sign(algorithm, data, key) / crypto.verify(...) (Node ≥ 12).
183
295
  eachMatch(RE_ONESHOT_SIGN_VERIFY, content, (m) => {
184
- push(
185
- {
186
- ruleId: "node-crypto-sign-oneshot",
187
- title: "Classical one-shot signature (crypto.sign/verify)",
188
- category: "signature",
189
- severity: "high",
190
- confidence: "medium",
191
- algorithm: "unknown",
192
- hndl: false,
193
- cwe: CWE_BROKEN_CRYPTO,
194
- message:
195
- "Uses the one-shot crypto.sign/crypto.verify API, typically RSA/ECDSA/EdDSA — forgeable by a quantum attacker.",
196
- remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
197
- },
198
- m,
296
+ findings.push(
297
+ findingFromRule(RULE_NODE_SIGN_ONESHOT, {
298
+ file,
299
+ content,
300
+ index: m.index,
301
+ matchLength: m[0].length,
302
+ }),
199
303
  );
200
304
  });
201
305
 
202
306
  // createDiffieHellman / createDiffieHellmanGroup — finite-field DH key exchange.
203
307
  eachMatch(RE_CREATE_DH, content, (m) => {
204
- push(
205
- {
206
- ruleId: "node-crypto-dh",
207
- title: "Diffie-Hellman key exchange",
208
- category: "key-exchange",
209
- severity: "high",
210
- confidence: "high",
211
- algorithm: "DH",
212
- hndl: true,
213
- cwe: CWE_BROKEN_CRYPTO,
214
- message:
215
- "Finite-field Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
216
- },
217
- m,
308
+ findings.push(
309
+ findingFromRule(RULE_NODE_DH, { file, content, index: m.index, matchLength: m[0].length }),
218
310
  );
219
311
  });
220
312
 
221
313
  // getDiffieHellman('modpN') — named built-in finite-field MODP groups.
222
314
  eachMatch(RE_GET_DH, content, (m) => {
223
- push(
224
- {
225
- ruleId: "node-crypto-dh-modp",
226
- title: `Diffie-Hellman MODP group (${m[1]})`,
227
- category: "key-exchange",
228
- severity: "high",
229
- confidence: "high",
230
- algorithm: "DH",
231
- hndl: true,
232
- cwe: CWE_BROKEN_CRYPTO,
233
- message: `Named finite-field DH MODP group "${m[1]}" is broken by Shor's algorithm (harvest-now-decrypt-later).`,
234
- },
235
- m,
315
+ findings.push(
316
+ findingFromRule(
317
+ RULE_NODE_DH_MODP,
318
+ { file, content, index: m.index, matchLength: m[0].length },
319
+ {
320
+ title: `Diffie-Hellman MODP group (${m[1]})`,
321
+ message: `Named finite-field DH MODP group "${m[1]}" is broken by Shor's algorithm (harvest-now-decrypt-later).`,
322
+ },
323
+ ),
236
324
  );
237
325
  });
238
326
 
239
327
  // createECDH — elliptic-curve Diffie-Hellman key exchange.
240
328
  eachMatch(RE_CREATE_ECDH, content, (m) => {
241
- push(
242
- {
243
- ruleId: "node-crypto-ecdh",
244
- title: "ECDH key exchange",
245
- category: "key-exchange",
246
- severity: "high",
247
- confidence: "high",
248
- algorithm: "ECDH",
249
- hndl: true,
250
- cwe: CWE_BROKEN_CRYPTO,
251
- message:
252
- "Elliptic-curve Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
253
- },
254
- m,
329
+ findings.push(
330
+ findingFromRule(RULE_NODE_ECDH, {
331
+ file,
332
+ content,
333
+ index: m.index,
334
+ matchLength: m[0].length,
335
+ }),
255
336
  );
256
337
  });
257
338
 
258
339
  // publicEncrypt / privateDecrypt — RSA encryption (KEM-like confidentiality).
259
340
  eachMatch(RE_RSA_ENCRYPT, content, (m) => {
260
- push(
261
- {
262
- ruleId: "node-crypto-rsa-encrypt",
263
- title: "RSA public-key encryption",
264
- category: "kem",
265
- severity: "high",
266
- confidence: "high",
267
- algorithm: "RSA",
268
- hndl: true,
269
- cwe: CWE_BROKEN_CRYPTO,
270
- message:
271
- "RSA public-key encryption is broken by Shor's algorithm and exposed to harvest-now-decrypt-later.",
272
- },
273
- m,
341
+ findings.push(
342
+ findingFromRule(RULE_NODE_RSA_ENCRYPT, {
343
+ file,
344
+ content,
345
+ index: m.index,
346
+ matchLength: m[0].length,
347
+ }),
274
348
  );
275
349
  });
276
350
 
277
351
  // diffieHellman({ privateKey, publicKey }) — KeyObject-based DH/ECDH.
278
352
  eachMatch(RE_DH_KEYOBJECT, content, (m) => {
279
- push(
280
- {
281
- ruleId: "node-crypto-dh-keyobject",
282
- title: "Diffie-Hellman (KeyObject) key exchange",
283
- category: "key-exchange",
284
- severity: "high",
285
- confidence: "high",
286
- algorithm: "ECDH",
287
- hndl: true,
288
- cwe: CWE_BROKEN_CRYPTO,
289
- message:
290
- "crypto.diffieHellman() performs a classical (EC)DH agreement (harvest-now-decrypt-later).",
291
- },
292
- m,
353
+ findings.push(
354
+ findingFromRule(RULE_NODE_DH_KEYOBJECT, {
355
+ file,
356
+ content,
357
+ index: m.index,
358
+ matchLength: m[0].length,
359
+ }),
293
360
  );
294
361
  });
295
362
 
@@ -301,6 +368,19 @@ const nodeCryptoDetector: Detector = {
301
368
  /* WebCrypto (SubtleCrypto) */
302
369
  /* -------------------------------------------------------------------------- */
303
370
 
371
+ const RULE_WEBCRYPTO: RuleMeta = {
372
+ id: "webcrypto-classical",
373
+ title: "WebCrypto classical algorithm",
374
+ description: "classical asymmetric algorithm passed to SubtleCrypto",
375
+ category: "signature",
376
+ severity: "high",
377
+ confidence: "high",
378
+ algorithm: "unknown",
379
+ hndl: false,
380
+ cwe: CWE_BROKEN_CRYPTO,
381
+ message: "A classical asymmetric WebCrypto algorithm is used, which is not quantum-safe.",
382
+ };
383
+
304
384
  /**
305
385
  * Detects classical algorithms passed to WebCrypto's SubtleCrypto methods. The
306
386
  * algorithm name can appear as a bare string ("RSA-OAEP") or as
@@ -311,6 +391,7 @@ const webCryptoDetector: Detector = {
311
391
  description: "Classical asymmetric algorithms via WebCrypto SubtleCrypto",
312
392
  scope: "source",
313
393
  language: "js",
394
+ rules: [RULE_WEBCRYPTO],
314
395
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
315
396
  detect({ file, content }): Finding[] {
316
397
  const findings: Finding[] = [];
@@ -335,21 +416,17 @@ const webCryptoDetector: Detector = {
335
416
  const category: Finding["category"] = isEcdh ? "key-exchange" : isKem ? "kem" : "signature";
336
417
  const hndl = isKem || isEcdh;
337
418
  findings.push(
338
- makeFinding({
339
- ruleId: "webcrypto-classical",
340
- title: `WebCrypto ${m[1]}`,
341
- category,
342
- severity: "high",
343
- confidence: "high",
344
- algorithm,
345
- hndl,
346
- cwe: CWE_BROKEN_CRYPTO,
347
- message: `WebCrypto algorithm "${m[1]}" is classical asymmetric crypto and not quantum-safe.`,
348
- file,
349
- content,
350
- index: m.index,
351
- matchLength: m[0].length,
352
- }),
419
+ findingFromRule(
420
+ RULE_WEBCRYPTO,
421
+ { file, content, index: m.index, matchLength: m[0].length },
422
+ {
423
+ title: `WebCrypto ${m[1]}`,
424
+ category,
425
+ algorithm,
426
+ hndl,
427
+ message: `WebCrypto algorithm "${m[1]}" is classical asymmetric crypto and not quantum-safe.`,
428
+ },
429
+ ),
353
430
  );
354
431
  });
355
432
 
@@ -361,119 +438,129 @@ const webCryptoDetector: Detector = {
361
438
  /* Popular crypto libraries */
362
439
  /* -------------------------------------------------------------------------- */
363
440
 
441
+ const RULE_FORGE_RSA: RuleMeta = {
442
+ id: "forge-rsa-keygen",
443
+ title: "node-forge RSA key generation",
444
+ description: "node-forge pki.rsa.generateKeyPair",
445
+ category: "kem",
446
+ severity: "high",
447
+ confidence: "high",
448
+ algorithm: "RSA",
449
+ hndl: true,
450
+ cwe: CWE_BROKEN_CRYPTO,
451
+ message: "node-forge generates a classical RSA key pair, which is not quantum-safe.",
452
+ };
453
+ const RULE_FORGE_ED25519: RuleMeta = {
454
+ id: "forge-ed25519",
455
+ title: "node-forge Ed25519 usage",
456
+ description: "node-forge forge.ed25519.*",
457
+ category: "signature",
458
+ severity: "low",
459
+ confidence: "high",
460
+ algorithm: "EdDSA",
461
+ hndl: false,
462
+ cwe: CWE_BROKEN_CRYPTO,
463
+ message: "node-forge Ed25519 is a modern but still classical signature scheme.",
464
+ };
465
+ const RULE_ELLIPTIC_EC: RuleMeta = {
466
+ id: "elliptic-ec",
467
+ title: "elliptic curve instantiation",
468
+ description: "the `elliptic` library — new EC(...)",
469
+ category: "signature",
470
+ severity: "high",
471
+ confidence: "high",
472
+ algorithm: "ECDSA",
473
+ hndl: false,
474
+ cwe: CWE_BROKEN_CRYPTO,
475
+ message:
476
+ "The `elliptic` library implements classical ECDSA/ECDH, both broken by Shor's algorithm.",
477
+ };
478
+ const RULE_SECP256K1: RuleMeta = {
479
+ id: "secp256k1-usage",
480
+ title: "secp256k1 ECDSA/ECDH usage",
481
+ description: "direct @noble/secp256k1-style API usage",
482
+ category: "signature",
483
+ severity: "high",
484
+ confidence: "medium",
485
+ algorithm: "ECDSA",
486
+ hndl: false,
487
+ cwe: CWE_BROKEN_CRYPTO,
488
+ message:
489
+ "Direct secp256k1 usage (ECDSA signatures / ECDH agreement) is classical and broken by Shor's algorithm.",
490
+ remediation: "ML-DSA-65 (FIPS 204) for signatures; hybrid X25519MLKEM768 for key agreement.",
491
+ };
492
+ const RULE_JSRSASIGN_KEYGEN: RuleMeta = {
493
+ id: "jsrsasign-keygen",
494
+ title: "jsrsasign key generation",
495
+ description: "jsrsasign KEYUTIL.generateKeypair",
496
+ category: "signature",
497
+ severity: "high",
498
+ confidence: "high",
499
+ algorithm: "unknown",
500
+ hndl: false,
501
+ cwe: CWE_BROKEN_CRYPTO,
502
+ message: "jsrsasign generates classical RSA/EC key pairs, which are not quantum-safe.",
503
+ remediation: "ML-KEM-768 (FIPS 203) / ML-DSA-65 (FIPS 204)",
504
+ };
505
+ const RULE_JSRSASIGN_SIGN: RuleMeta = {
506
+ id: "jsrsasign-sign",
507
+ title: "jsrsasign signature",
508
+ description: "jsrsasign KJUR.crypto.Signature / ECDSA",
509
+ category: "signature",
510
+ severity: "high",
511
+ confidence: "high",
512
+ algorithm: "unknown",
513
+ hndl: false,
514
+ cwe: CWE_BROKEN_CRYPTO,
515
+ message:
516
+ "jsrsasign signing uses classical RSA/ECDSA signatures, forgeable by a quantum attacker.",
517
+ remediation: "ML-DSA-65 (FIPS 204)",
518
+ };
519
+ const RULE_NODE_RSA_LIB: RuleMeta = {
520
+ id: "node-rsa",
521
+ title: "node-rsa key/usage",
522
+ description: "the `node-rsa` library — new NodeRSA(...)",
523
+ category: "kem",
524
+ severity: "high",
525
+ confidence: "high",
526
+ algorithm: "RSA",
527
+ hndl: true,
528
+ cwe: CWE_BROKEN_CRYPTO,
529
+ message: "node-rsa wraps classical RSA encryption/signing, which is not quantum-safe.",
530
+ };
531
+
364
532
  /** Detects classical crypto from popular npm libraries used in source. */
365
533
  const libraryDetector: Detector = {
366
534
  id: "crypto-libs",
367
535
  description: "Classical asymmetric crypto via node-forge, elliptic, jsrsasign, node-rsa",
368
536
  scope: "source",
369
537
  language: "js",
538
+ rules: [
539
+ RULE_FORGE_RSA,
540
+ RULE_FORGE_ED25519,
541
+ RULE_ELLIPTIC_EC,
542
+ RULE_SECP256K1,
543
+ RULE_JSRSASIGN_KEYGEN,
544
+ RULE_JSRSASIGN_SIGN,
545
+ RULE_NODE_RSA_LIB,
546
+ ],
370
547
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
371
548
  detect({ file, content }): Finding[] {
372
549
  const findings: Finding[] = [];
373
- const add = (
374
- re: RegExp,
375
- spec: Omit<Parameters<typeof makeFinding>[0], "file" | "content" | "index" | "matchLength">,
376
- ) =>
550
+ const add = (re: RegExp, rule: RuleMeta) =>
377
551
  eachMatch(re, content, (m) =>
378
552
  findings.push(
379
- makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length }),
553
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
380
554
  ),
381
555
  );
382
556
 
383
- // node-forge: pki.rsa.generateKeyPair(...)
384
- add(RE_FORGE_RSA, {
385
- ruleId: "forge-rsa-keygen",
386
- title: "node-forge RSA key generation",
387
- category: "kem",
388
- severity: "high",
389
- confidence: "high",
390
- algorithm: "RSA",
391
- hndl: true,
392
- cwe: CWE_BROKEN_CRYPTO,
393
- message: "node-forge generates a classical RSA key pair, which is not quantum-safe.",
394
- });
395
-
396
- // node-forge: forge.ed25519.* (classical EdDSA)
397
- add(RE_FORGE_ED25519, {
398
- ruleId: "forge-ed25519",
399
- title: "node-forge Ed25519 usage",
400
- category: "signature",
401
- severity: "low",
402
- confidence: "high",
403
- algorithm: "EdDSA",
404
- hndl: false,
405
- cwe: CWE_BROKEN_CRYPTO,
406
- message: "node-forge Ed25519 is a modern but still classical signature scheme.",
407
- });
408
-
409
- // elliptic: new EC('secp256k1') / new ec(...)
410
- add(RE_ELLIPTIC_EC, {
411
- ruleId: "elliptic-ec",
412
- title: "elliptic curve instantiation",
413
- category: "signature",
414
- severity: "high",
415
- confidence: "high",
416
- algorithm: "ECDSA",
417
- hndl: false,
418
- cwe: CWE_BROKEN_CRYPTO,
419
- message:
420
- "The `elliptic` library implements classical ECDSA/ECDH, both broken by Shor's algorithm.",
421
- });
422
-
423
- // Direct secp256k1 API usage: secp.sign / getPublicKey / getSharedSecret.
424
- add(RE_SECP256K1, {
425
- ruleId: "secp256k1-usage",
426
- title: "secp256k1 ECDSA/ECDH usage",
427
- category: "signature",
428
- severity: "high",
429
- confidence: "medium",
430
- algorithm: "ECDSA",
431
- hndl: false,
432
- cwe: CWE_BROKEN_CRYPTO,
433
- message:
434
- "Direct secp256k1 usage (ECDSA signatures / ECDH agreement) is classical and broken by Shor's algorithm.",
435
- remediation: "ML-DSA-65 (FIPS 204) for signatures; hybrid X25519MLKEM768 for key agreement.",
436
- });
437
-
438
- // jsrsasign: KEYUTIL.generateKeypair('RSA'|'EC', ...) or KJUR.crypto.*
439
- add(RE_JSRSASIGN_KEYGEN, {
440
- ruleId: "jsrsasign-keygen",
441
- title: "jsrsasign key generation",
442
- category: "signature",
443
- severity: "high",
444
- confidence: "high",
445
- algorithm: "unknown",
446
- hndl: false,
447
- cwe: CWE_BROKEN_CRYPTO,
448
- message: "jsrsasign generates classical RSA/EC key pairs, which are not quantum-safe.",
449
- remediation: "ML-KEM-768 (FIPS 203) / ML-DSA-65 (FIPS 204)",
450
- });
451
- add(RE_JSRSASIGN_SIGN, {
452
- ruleId: "jsrsasign-sign",
453
- title: "jsrsasign signature",
454
- category: "signature",
455
- severity: "high",
456
- confidence: "high",
457
- algorithm: "unknown",
458
- hndl: false,
459
- cwe: CWE_BROKEN_CRYPTO,
460
- message:
461
- "jsrsasign signing uses classical RSA/ECDSA signatures, forgeable by a quantum attacker.",
462
- remediation: "ML-DSA-65 (FIPS 204)",
463
- });
464
-
465
- // node-rsa: new NodeRSA(...)
466
- add(RE_NODE_RSA, {
467
- ruleId: "node-rsa",
468
- title: "node-rsa key/usage",
469
- category: "kem",
470
- severity: "high",
471
- confidence: "high",
472
- algorithm: "RSA",
473
- hndl: true,
474
- cwe: CWE_BROKEN_CRYPTO,
475
- message: "node-rsa wraps classical RSA encryption/signing, which is not quantum-safe.",
476
- });
557
+ add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
558
+ add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
559
+ add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
560
+ add(RE_SECP256K1, RULE_SECP256K1); // secp.sign / getPublicKey / getSharedSecret
561
+ add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
562
+ add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
563
+ add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
477
564
 
478
565
  return findings;
479
566
  },
@@ -483,6 +570,34 @@ const libraryDetector: Detector = {
483
570
  /* JWT / JOSE / COSE algorithm strings */
484
571
  /* -------------------------------------------------------------------------- */
485
572
 
573
+ const RULE_JWT_ALG: RuleMeta = {
574
+ id: "jwt-classical-alg",
575
+ title: "Classical JWT/JOSE algorithm",
576
+ description: "JWS alg tokens (RS/PS/ES/EdDSA)",
577
+ category: "signature",
578
+ severity: "high",
579
+ confidence: "medium",
580
+ algorithm: "unknown",
581
+ hndl: false,
582
+ cwe: CWE_BROKEN_CRYPTO,
583
+ message: "A classical JWT/JOSE signature algorithm is used, forgeable by a quantum attacker.",
584
+ remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
585
+ };
586
+ const RULE_JOSE_ECDH: RuleMeta = {
587
+ id: "jose-ecdh-es",
588
+ title: "JOSE ECDH-ES key agreement",
589
+ description: "JOSE ECDH-ES / ECDH-ES+A*KW key agreement",
590
+ category: "key-exchange",
591
+ severity: "high",
592
+ confidence: "medium",
593
+ algorithm: "ECDH",
594
+ hndl: true,
595
+ cwe: CWE_BROKEN_CRYPTO,
596
+ message:
597
+ "JOSE ECDH-ES performs classical ECDH key agreement — harvest-now-decrypt-later exposed.",
598
+ remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
599
+ };
600
+
486
601
  /**
487
602
  * Detects classical signature algorithm identifiers used by JWT/JOSE, plus
488
603
  * ECDH-ES key-agreement identifiers (HNDL-exposed). These appear as string
@@ -493,6 +608,7 @@ const jwtDetector: Detector = {
493
608
  description: "Classical JWT/JOSE algorithms (RS/PS/ES/EdDSA) and ECDH-ES key agreement",
494
609
  scope: "source",
495
610
  language: "js",
611
+ rules: [RULE_JWT_ALG, RULE_JOSE_ECDH],
496
612
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
497
613
  detect({ file, content }): Finding[] {
498
614
  const findings: Finding[] = [];
@@ -505,45 +621,29 @@ const jwtDetector: Detector = {
505
621
  else if (alg === "EdDSA") algorithm = "EdDSA";
506
622
  else algorithm = "ECDSA"; // ES*
507
623
  findings.push(
508
- makeFinding({
509
- ruleId: "jwt-classical-alg",
510
- title: `JWT/JOSE algorithm ${alg}`,
511
- category: "signature",
512
- severity: "high",
513
- confidence: "medium",
514
- algorithm,
515
- hndl: false,
516
- cwe: CWE_BROKEN_CRYPTO,
517
- message: `JWT/JOSE algorithm "${alg}" is a classical signature, forgeable by a quantum attacker.`,
518
- remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
519
- file,
520
- content,
521
- index: m.index,
522
- matchLength: m[0].length,
523
- }),
624
+ findingFromRule(
625
+ RULE_JWT_ALG,
626
+ { file, content, index: m.index, matchLength: m[0].length },
627
+ {
628
+ title: `JWT/JOSE algorithm ${alg}`,
629
+ algorithm,
630
+ message: `JWT/JOSE algorithm "${alg}" is a classical signature, forgeable by a quantum attacker.`,
631
+ },
632
+ ),
524
633
  );
525
634
  });
526
635
 
527
636
  // JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
528
637
  eachMatch(RE_JOSE_ECDH, content, (m) => {
529
638
  findings.push(
530
- makeFinding({
531
- ruleId: "jose-ecdh-es",
532
- title: `JOSE key agreement ${m[1]}`,
533
- category: "key-exchange",
534
- severity: "high",
535
- confidence: "medium",
536
- algorithm: "ECDH",
537
- hndl: true,
538
- cwe: CWE_BROKEN_CRYPTO,
539
- message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
540
- remediation:
541
- "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
542
- file,
543
- content,
544
- index: m.index,
545
- matchLength: m[0].length,
546
- }),
639
+ findingFromRule(
640
+ RULE_JOSE_ECDH,
641
+ { file, content, index: m.index, matchLength: m[0].length },
642
+ {
643
+ title: `JOSE key agreement ${m[1]}`,
644
+ message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
645
+ },
646
+ ),
547
647
  );
548
648
  });
549
649
 
@@ -555,6 +655,43 @@ const jwtDetector: Detector = {
555
655
  /* TLS legacy configuration */
556
656
  /* -------------------------------------------------------------------------- */
557
657
 
658
+ const RULE_TLS_LEGACY: RuleMeta = {
659
+ id: "tls-legacy-version",
660
+ title: "Legacy TLS version pinned",
661
+ description: "minVersion/maxVersion/secureProtocol pinned to TLS 1.0/1.1",
662
+ category: "tls",
663
+ severity: "medium",
664
+ confidence: "high",
665
+ hndl: false,
666
+ cwe: CWE_WEAK_STRENGTH,
667
+ message: "TLS 1.0/1.1 are deprecated and insecure; require TLS 1.3.",
668
+ remediation: "Set minVersion: 'TLSv1.3' and prefer PQC-hybrid key exchange.",
669
+ };
670
+ const RULE_TLS_REJECT: RuleMeta = {
671
+ id: "tls-reject-unauthorized",
672
+ title: "TLS certificate verification disabled",
673
+ description: "rejectUnauthorized: false",
674
+ category: "tls",
675
+ severity: "high",
676
+ confidence: "high",
677
+ hndl: false,
678
+ cwe: CWE_CERT_VALIDATION,
679
+ message: "rejectUnauthorized:false disables TLS certificate verification (MITM risk).",
680
+ remediation: "Remove rejectUnauthorized:false; verify certificates properly.",
681
+ };
682
+ const RULE_TLS_WEAK_CIPHER: RuleMeta = {
683
+ id: "tls-weak-cipher",
684
+ title: "Weak TLS cipher configured",
685
+ description: "weak/export cipher in a `ciphers` string",
686
+ category: "tls",
687
+ severity: "medium",
688
+ confidence: "medium",
689
+ hndl: false,
690
+ cwe: CWE_WEAK_STRENGTH,
691
+ message: "A weak cipher is configured in the TLS ciphers list.",
692
+ remediation: "Use a modern AEAD cipher suite (TLS 1.3 defaults).",
693
+ };
694
+
558
695
  /**
559
696
  * Detects legacy / insecure TLS configuration expressed as JS object literals:
560
697
  * forced TLS 1.0/1.1, disabled certificate verification, and weak ciphers.
@@ -566,6 +703,7 @@ const tlsDetector: Detector = {
566
703
  description: "Legacy / insecure TLS configuration in JS objects",
567
704
  scope: "config",
568
705
  language: "js",
706
+ rules: [RULE_TLS_LEGACY, RULE_TLS_REJECT, RULE_TLS_WEAK_CIPHER],
569
707
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
570
708
  detect({ file, content }): Finding[] {
571
709
  const findings: Finding[] = [];
@@ -573,16 +711,7 @@ const tlsDetector: Detector = {
573
711
  // minVersion / maxVersion / secureProtocol pinned to TLS 1.0 or 1.1.
574
712
  eachMatch(RE_TLS_LEGACY_VERSION, content, (m) => {
575
713
  findings.push(
576
- makeFinding({
577
- ruleId: "tls-legacy-version",
578
- title: "Legacy TLS version pinned",
579
- category: "tls",
580
- severity: "medium",
581
- confidence: "high",
582
- hndl: false,
583
- cwe: CWE_WEAK_STRENGTH,
584
- message: "TLS 1.0/1.1 are deprecated and insecure; require TLS 1.3.",
585
- remediation: "Set minVersion: 'TLSv1.3' and prefer PQC-hybrid key exchange.",
714
+ findingFromRule(RULE_TLS_LEGACY, {
586
715
  file,
587
716
  content,
588
717
  index: m.index,
@@ -594,16 +723,7 @@ const tlsDetector: Detector = {
594
723
  // rejectUnauthorized: false — disables certificate verification.
595
724
  eachMatch(RE_TLS_REJECT, content, (m) => {
596
725
  findings.push(
597
- makeFinding({
598
- ruleId: "tls-reject-unauthorized",
599
- title: "TLS certificate verification disabled",
600
- category: "tls",
601
- severity: "high",
602
- confidence: "high",
603
- hndl: false,
604
- cwe: CWE_CERT_VALIDATION,
605
- message: "rejectUnauthorized:false disables TLS certificate verification (MITM risk).",
606
- remediation: "Remove rejectUnauthorized:false; verify certificates properly.",
726
+ findingFromRule(RULE_TLS_REJECT, {
607
727
  file,
608
728
  content,
609
729
  index: m.index,
@@ -615,21 +735,11 @@ const tlsDetector: Detector = {
615
735
  // Weak / export ciphers referenced in a ciphers string (bounded regex).
616
736
  eachMatch(RE_TLS_WEAK_CIPHER, content, (m) => {
617
737
  findings.push(
618
- makeFinding({
619
- ruleId: "tls-weak-cipher",
620
- title: "Weak TLS cipher configured",
621
- category: "tls",
622
- severity: "medium",
623
- confidence: "medium",
624
- hndl: false,
625
- cwe: CWE_WEAK_STRENGTH,
626
- message: `Weak cipher (${m[1]}) configured in the TLS ciphers list.`,
627
- remediation: "Use a modern AEAD cipher suite (TLS 1.3 defaults).",
628
- file,
629
- content,
630
- index: m.index,
631
- matchLength: m[0].length,
632
- }),
738
+ findingFromRule(
739
+ RULE_TLS_WEAK_CIPHER,
740
+ { file, content, index: m.index, matchLength: m[0].length },
741
+ { message: `Weak cipher (${m[1]}) configured in the TLS ciphers list.` },
742
+ ),
633
743
  );
634
744
  });
635
745
 
@@ -645,6 +755,35 @@ const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384
645
755
  const RE_CERT_SIG_ALG =
646
756
  /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA(?:1|256|384|512)|rsassaPss|dsaWithSHA(?:1|256))\b/g;
647
757
 
758
+ const RULE_SSH_PUBKEY: RuleMeta = {
759
+ id: "ssh-public-key",
760
+ title: "Classical SSH public key",
761
+ description: "ssh-rsa / ssh-ed25519 / ssh-dss / ecdsa-sha2-* public keys",
762
+ category: "certificate",
763
+ severity: "low",
764
+ confidence: "medium",
765
+ algorithm: "unknown",
766
+ hndl: false,
767
+ cwe: CWE_BROKEN_CRYPTO,
768
+ sensitive: true,
769
+ message: "A classical SSH public key is forgeable by a quantum attacker.",
770
+ remediation: "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
771
+ };
772
+ const RULE_CERT_SIG_ALG: RuleMeta = {
773
+ id: "cert-signature-algorithm",
774
+ title: "Classical certificate signature algorithm",
775
+ description: "X.509/TLS certificate signature-algorithm identifiers",
776
+ category: "certificate",
777
+ severity: "low",
778
+ confidence: "medium",
779
+ algorithm: "unknown",
780
+ hndl: false,
781
+ cwe: CWE_BROKEN_CRYPTO,
782
+ message:
783
+ "A classical certificate signature algorithm (RSA/ECDSA/DSA) is a quantum forgery surface.",
784
+ remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
785
+ };
786
+
648
787
  /**
649
788
  * Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
650
789
  * and X.509 certificate signature-algorithm identifiers in any text file. These
@@ -656,6 +795,7 @@ const sshCertDetector: Detector = {
656
795
  description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
657
796
  scope: "config",
658
797
  language: "any",
798
+ rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG],
659
799
  appliesTo: () => true,
660
800
  detect({ file, content }): Finding[] {
661
801
  const findings: Finding[] = [];
@@ -671,24 +811,15 @@ const sshCertDetector: Detector = {
671
811
  ? "DSA"
672
812
  : "ECDSA";
673
813
  findings.push(
674
- makeFinding({
675
- ruleId: "ssh-public-key",
676
- title: `Classical SSH public key (${tok})`,
677
- category: "certificate",
678
- severity: "low",
679
- confidence: "medium",
680
- algorithm,
681
- hndl: false,
682
- cwe: CWE_BROKEN_CRYPTO,
683
- sensitive: true,
684
- message: `SSH public key type "${tok}" is a classical key forgeable by a quantum attacker.`,
685
- remediation:
686
- "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
687
- file,
688
- content,
689
- index: m.index,
690
- matchLength: m[0].length,
691
- }),
814
+ findingFromRule(
815
+ RULE_SSH_PUBKEY,
816
+ { file, content, index: m.index, matchLength: m[0].length },
817
+ {
818
+ title: `Classical SSH public key (${tok})`,
819
+ algorithm,
820
+ message: `SSH public key type "${tok}" is a classical key forgeable by a quantum attacker.`,
821
+ },
822
+ ),
692
823
  );
693
824
  });
694
825
 
@@ -701,23 +832,15 @@ const sshCertDetector: Detector = {
701
832
  ? "ECDSA"
702
833
  : "DSA";
703
834
  findings.push(
704
- makeFinding({
705
- ruleId: "cert-signature-algorithm",
706
- title: `Classical certificate signature algorithm (${tok})`,
707
- category: "certificate",
708
- severity: "low",
709
- confidence: "medium",
710
- algorithm,
711
- hndl: false,
712
- cwe: CWE_BROKEN_CRYPTO,
713
- message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
714
- remediation:
715
- "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
716
- file,
717
- content,
718
- index: m.index,
719
- matchLength: m[0].length,
720
- }),
835
+ findingFromRule(
836
+ RULE_CERT_SIG_ALG,
837
+ { file, content, index: m.index, matchLength: m[0].length },
838
+ {
839
+ title: `Classical certificate signature algorithm (${tok})`,
840
+ algorithm,
841
+ message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
842
+ },
843
+ ),
721
844
  );
722
845
  });
723
846