@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,4 +1,4 @@
1
- import { JS_TS_EXTENSIONS, eachMatch, hasExtension, makeFinding, nearSortedCall, } from "../detect-utils.js";
1
+ import { JS_TS_EXTENSIONS, eachMatch, findingFromRule, hasExtension, nearSortedCall, } from "../detect-utils.js";
2
2
  import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
3
3
  /* -------------------------------------------------------------------------- */
4
4
  /* Precompiled regexes (module scope — never recreated per file) */
@@ -42,16 +42,128 @@ const RE_TLS_WEAK_CIPHER = /ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(RC4|DES|3DES|M
42
42
  /* -------------------------------------------------------------------------- */
43
43
  /* Node.js `crypto` module */
44
44
  /* -------------------------------------------------------------------------- */
45
+ /**
46
+ * Rule catalog for the Node `crypto` detector. `node-crypto-keygen` and
47
+ * `node-crypto-dh-modp` refine some fields per match; the rest emit findings
48
+ * straight from these declarations.
49
+ */
50
+ const RULE_NODE_KEYGEN = {
51
+ id: "node-crypto-keygen",
52
+ title: "Classical key generation",
53
+ description: "crypto.generateKeyPair(Sync)('rsa'|'ec'|'dsa'|'dh'|…)",
54
+ category: "key-exchange",
55
+ severity: "high",
56
+ confidence: "high",
57
+ algorithm: "unknown",
58
+ hndl: true,
59
+ cwe: CWE_BROKEN_CRYPTO,
60
+ message: "Generates a classical asymmetric key pair, which is not quantum-safe.",
61
+ };
62
+ const RULE_NODE_SIGN = {
63
+ id: "node-crypto-sign",
64
+ title: "Classical signature (createSign/createVerify)",
65
+ description: "crypto.createSign / crypto.createVerify",
66
+ category: "signature",
67
+ severity: "high",
68
+ confidence: "medium",
69
+ algorithm: "unknown",
70
+ hndl: false,
71
+ cwe: CWE_BROKEN_CRYPTO,
72
+ message: "Uses createSign/createVerify, typically RSA, ECDSA or DSA — all forgeable by a quantum attacker.",
73
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
74
+ };
75
+ const RULE_NODE_SIGN_ONESHOT = {
76
+ id: "node-crypto-sign-oneshot",
77
+ title: "Classical one-shot signature (crypto.sign/verify)",
78
+ description: "one-shot crypto.sign / crypto.verify (Node ≥ 12)",
79
+ category: "signature",
80
+ severity: "high",
81
+ confidence: "medium",
82
+ algorithm: "unknown",
83
+ hndl: false,
84
+ cwe: CWE_BROKEN_CRYPTO,
85
+ message: "Uses the one-shot crypto.sign/crypto.verify API, typically RSA/ECDSA/EdDSA — forgeable by a quantum attacker.",
86
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
87
+ };
88
+ const RULE_NODE_DH = {
89
+ id: "node-crypto-dh",
90
+ title: "Diffie-Hellman key exchange",
91
+ description: "crypto.createDiffieHellman(Group)",
92
+ category: "key-exchange",
93
+ severity: "high",
94
+ confidence: "high",
95
+ algorithm: "DH",
96
+ hndl: true,
97
+ cwe: CWE_BROKEN_CRYPTO,
98
+ message: "Finite-field Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
99
+ };
100
+ const RULE_NODE_DH_MODP = {
101
+ id: "node-crypto-dh-modp",
102
+ title: "Diffie-Hellman MODP group",
103
+ description: "crypto.getDiffieHellman('modpN')",
104
+ category: "key-exchange",
105
+ severity: "high",
106
+ confidence: "high",
107
+ algorithm: "DH",
108
+ hndl: true,
109
+ cwe: CWE_BROKEN_CRYPTO,
110
+ message: "Named finite-field DH MODP group is broken by Shor's algorithm (harvest-now-decrypt-later).",
111
+ };
112
+ const RULE_NODE_ECDH = {
113
+ id: "node-crypto-ecdh",
114
+ title: "ECDH key exchange",
115
+ description: "crypto.createECDH",
116
+ category: "key-exchange",
117
+ severity: "high",
118
+ confidence: "high",
119
+ algorithm: "ECDH",
120
+ hndl: true,
121
+ cwe: CWE_BROKEN_CRYPTO,
122
+ message: "Elliptic-curve Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
123
+ };
124
+ const RULE_NODE_RSA_ENCRYPT = {
125
+ id: "node-crypto-rsa-encrypt",
126
+ title: "RSA public-key encryption",
127
+ description: "crypto.publicEncrypt / crypto.privateDecrypt",
128
+ category: "kem",
129
+ severity: "high",
130
+ confidence: "high",
131
+ algorithm: "RSA",
132
+ hndl: true,
133
+ cwe: CWE_BROKEN_CRYPTO,
134
+ message: "RSA public-key encryption is broken by Shor's algorithm and exposed to harvest-now-decrypt-later.",
135
+ };
136
+ const RULE_NODE_DH_KEYOBJECT = {
137
+ id: "node-crypto-dh-keyobject",
138
+ title: "Diffie-Hellman (KeyObject) key exchange",
139
+ description: "crypto.diffieHellman({ privateKey, publicKey })",
140
+ category: "key-exchange",
141
+ severity: "high",
142
+ confidence: "high",
143
+ algorithm: "ECDH",
144
+ hndl: true,
145
+ cwe: CWE_BROKEN_CRYPTO,
146
+ message: "crypto.diffieHellman() performs a classical (EC)DH agreement (harvest-now-decrypt-later).",
147
+ };
45
148
  /** Detects classical asymmetric usage from Node's built-in `crypto` module. */
46
149
  const nodeCryptoDetector = {
47
150
  id: "node-crypto",
48
151
  description: "Classical asymmetric crypto via the Node.js `crypto` module",
49
152
  scope: "source",
50
153
  language: "js",
154
+ rules: [
155
+ RULE_NODE_KEYGEN,
156
+ RULE_NODE_SIGN,
157
+ RULE_NODE_SIGN_ONESHOT,
158
+ RULE_NODE_DH,
159
+ RULE_NODE_DH_MODP,
160
+ RULE_NODE_ECDH,
161
+ RULE_NODE_RSA_ENCRYPT,
162
+ RULE_NODE_DH_KEYOBJECT,
163
+ ],
51
164
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
52
165
  detect({ file, content }) {
53
166
  const findings = [];
54
- const push = (spec, m) => findings.push(makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length }));
55
167
  // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
56
168
  eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
57
169
  const type = m[1].toLowerCase();
@@ -78,119 +190,72 @@ const nodeCryptoDetector = {
78
190
  ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
79
191
  };
80
192
  const info = map[type];
81
- push({
82
- ruleId: "node-crypto-keygen",
193
+ findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index: m.index, matchLength: m[0].length }, {
83
194
  title: `${info.label} key generation`,
84
195
  category: info.cat,
85
196
  severity: info.sev,
86
- confidence: "high",
87
197
  algorithm: info.algo,
88
198
  hndl: info.hndl,
89
- cwe: CWE_BROKEN_CRYPTO,
90
199
  message: info.message ??
91
200
  `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
92
201
  ...(info.remediation ? { remediation: info.remediation } : {}),
93
- }, m);
202
+ }));
94
203
  });
95
204
  // createSign / createVerify — RSA / ECDSA / DSA signatures.
96
205
  eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
97
- push({
98
- ruleId: "node-crypto-sign",
99
- title: "Classical signature (createSign/createVerify)",
100
- category: "signature",
101
- severity: "high",
102
- confidence: "medium",
103
- algorithm: "unknown",
104
- hndl: false,
105
- cwe: CWE_BROKEN_CRYPTO,
106
- message: "Uses createSign/createVerify, typically RSA, ECDSA or DSA — all forgeable by a quantum attacker.",
107
- remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
108
- }, m);
206
+ findings.push(findingFromRule(RULE_NODE_SIGN, {
207
+ file,
208
+ content,
209
+ index: m.index,
210
+ matchLength: m[0].length,
211
+ }));
109
212
  });
110
213
  // One-shot crypto.sign(algorithm, data, key) / crypto.verify(...) (Node ≥ 12).
111
214
  eachMatch(RE_ONESHOT_SIGN_VERIFY, content, (m) => {
112
- push({
113
- ruleId: "node-crypto-sign-oneshot",
114
- title: "Classical one-shot signature (crypto.sign/verify)",
115
- category: "signature",
116
- severity: "high",
117
- confidence: "medium",
118
- algorithm: "unknown",
119
- hndl: false,
120
- cwe: CWE_BROKEN_CRYPTO,
121
- message: "Uses the one-shot crypto.sign/crypto.verify API, typically RSA/ECDSA/EdDSA — forgeable by a quantum attacker.",
122
- remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
123
- }, m);
215
+ findings.push(findingFromRule(RULE_NODE_SIGN_ONESHOT, {
216
+ file,
217
+ content,
218
+ index: m.index,
219
+ matchLength: m[0].length,
220
+ }));
124
221
  });
125
222
  // createDiffieHellman / createDiffieHellmanGroup — finite-field DH key exchange.
126
223
  eachMatch(RE_CREATE_DH, content, (m) => {
127
- push({
128
- ruleId: "node-crypto-dh",
129
- title: "Diffie-Hellman key exchange",
130
- category: "key-exchange",
131
- severity: "high",
132
- confidence: "high",
133
- algorithm: "DH",
134
- hndl: true,
135
- cwe: CWE_BROKEN_CRYPTO,
136
- message: "Finite-field Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
137
- }, m);
224
+ findings.push(findingFromRule(RULE_NODE_DH, { file, content, index: m.index, matchLength: m[0].length }));
138
225
  });
139
226
  // getDiffieHellman('modpN') — named built-in finite-field MODP groups.
140
227
  eachMatch(RE_GET_DH, content, (m) => {
141
- push({
142
- ruleId: "node-crypto-dh-modp",
228
+ findings.push(findingFromRule(RULE_NODE_DH_MODP, { file, content, index: m.index, matchLength: m[0].length }, {
143
229
  title: `Diffie-Hellman MODP group (${m[1]})`,
144
- category: "key-exchange",
145
- severity: "high",
146
- confidence: "high",
147
- algorithm: "DH",
148
- hndl: true,
149
- cwe: CWE_BROKEN_CRYPTO,
150
230
  message: `Named finite-field DH MODP group "${m[1]}" is broken by Shor's algorithm (harvest-now-decrypt-later).`,
151
- }, m);
231
+ }));
152
232
  });
153
233
  // createECDH — elliptic-curve Diffie-Hellman key exchange.
154
234
  eachMatch(RE_CREATE_ECDH, content, (m) => {
155
- push({
156
- ruleId: "node-crypto-ecdh",
157
- title: "ECDH key exchange",
158
- category: "key-exchange",
159
- severity: "high",
160
- confidence: "high",
161
- algorithm: "ECDH",
162
- hndl: true,
163
- cwe: CWE_BROKEN_CRYPTO,
164
- message: "Elliptic-curve Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
165
- }, m);
235
+ findings.push(findingFromRule(RULE_NODE_ECDH, {
236
+ file,
237
+ content,
238
+ index: m.index,
239
+ matchLength: m[0].length,
240
+ }));
166
241
  });
167
242
  // publicEncrypt / privateDecrypt — RSA encryption (KEM-like confidentiality).
168
243
  eachMatch(RE_RSA_ENCRYPT, content, (m) => {
169
- push({
170
- ruleId: "node-crypto-rsa-encrypt",
171
- title: "RSA public-key encryption",
172
- category: "kem",
173
- severity: "high",
174
- confidence: "high",
175
- algorithm: "RSA",
176
- hndl: true,
177
- cwe: CWE_BROKEN_CRYPTO,
178
- message: "RSA public-key encryption is broken by Shor's algorithm and exposed to harvest-now-decrypt-later.",
179
- }, m);
244
+ findings.push(findingFromRule(RULE_NODE_RSA_ENCRYPT, {
245
+ file,
246
+ content,
247
+ index: m.index,
248
+ matchLength: m[0].length,
249
+ }));
180
250
  });
181
251
  // diffieHellman({ privateKey, publicKey }) — KeyObject-based DH/ECDH.
182
252
  eachMatch(RE_DH_KEYOBJECT, content, (m) => {
183
- push({
184
- ruleId: "node-crypto-dh-keyobject",
185
- title: "Diffie-Hellman (KeyObject) key exchange",
186
- category: "key-exchange",
187
- severity: "high",
188
- confidence: "high",
189
- algorithm: "ECDH",
190
- hndl: true,
191
- cwe: CWE_BROKEN_CRYPTO,
192
- message: "crypto.diffieHellman() performs a classical (EC)DH agreement (harvest-now-decrypt-later).",
193
- }, m);
253
+ findings.push(findingFromRule(RULE_NODE_DH_KEYOBJECT, {
254
+ file,
255
+ content,
256
+ index: m.index,
257
+ matchLength: m[0].length,
258
+ }));
194
259
  });
195
260
  return findings;
196
261
  },
@@ -198,6 +263,18 @@ const nodeCryptoDetector = {
198
263
  /* -------------------------------------------------------------------------- */
199
264
  /* WebCrypto (SubtleCrypto) */
200
265
  /* -------------------------------------------------------------------------- */
266
+ const RULE_WEBCRYPTO = {
267
+ id: "webcrypto-classical",
268
+ title: "WebCrypto classical algorithm",
269
+ description: "classical asymmetric algorithm passed to SubtleCrypto",
270
+ category: "signature",
271
+ severity: "high",
272
+ confidence: "high",
273
+ algorithm: "unknown",
274
+ hndl: false,
275
+ cwe: CWE_BROKEN_CRYPTO,
276
+ message: "A classical asymmetric WebCrypto algorithm is used, which is not quantum-safe.",
277
+ };
201
278
  /**
202
279
  * Detects classical algorithms passed to WebCrypto's SubtleCrypto methods. The
203
280
  * algorithm name can appear as a bare string ("RSA-OAEP") or as
@@ -208,6 +285,7 @@ const webCryptoDetector = {
208
285
  description: "Classical asymmetric algorithms via WebCrypto SubtleCrypto",
209
286
  scope: "source",
210
287
  language: "js",
288
+ rules: [RULE_WEBCRYPTO],
211
289
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
212
290
  detect({ file, content }) {
213
291
  const findings = [];
@@ -231,20 +309,12 @@ const webCryptoDetector = {
231
309
  : "ECDSA";
232
310
  const category = isEcdh ? "key-exchange" : isKem ? "kem" : "signature";
233
311
  const hndl = isKem || isEcdh;
234
- findings.push(makeFinding({
235
- ruleId: "webcrypto-classical",
312
+ findings.push(findingFromRule(RULE_WEBCRYPTO, { file, content, index: m.index, matchLength: m[0].length }, {
236
313
  title: `WebCrypto ${m[1]}`,
237
314
  category,
238
- severity: "high",
239
- confidence: "high",
240
315
  algorithm,
241
316
  hndl,
242
- cwe: CWE_BROKEN_CRYPTO,
243
317
  message: `WebCrypto algorithm "${m[1]}" is classical asymmetric crypto and not quantum-safe.`,
244
- file,
245
- content,
246
- index: m.index,
247
- matchLength: m[0].length,
248
318
  }));
249
319
  });
250
320
  return findings;
@@ -253,108 +323,151 @@ const webCryptoDetector = {
253
323
  /* -------------------------------------------------------------------------- */
254
324
  /* Popular crypto libraries */
255
325
  /* -------------------------------------------------------------------------- */
326
+ const RULE_FORGE_RSA = {
327
+ id: "forge-rsa-keygen",
328
+ title: "node-forge RSA key generation",
329
+ description: "node-forge pki.rsa.generateKeyPair",
330
+ category: "kem",
331
+ severity: "high",
332
+ confidence: "high",
333
+ algorithm: "RSA",
334
+ hndl: true,
335
+ cwe: CWE_BROKEN_CRYPTO,
336
+ message: "node-forge generates a classical RSA key pair, which is not quantum-safe.",
337
+ };
338
+ const RULE_FORGE_ED25519 = {
339
+ id: "forge-ed25519",
340
+ title: "node-forge Ed25519 usage",
341
+ description: "node-forge forge.ed25519.*",
342
+ category: "signature",
343
+ severity: "low",
344
+ confidence: "high",
345
+ algorithm: "EdDSA",
346
+ hndl: false,
347
+ cwe: CWE_BROKEN_CRYPTO,
348
+ message: "node-forge Ed25519 is a modern but still classical signature scheme.",
349
+ };
350
+ const RULE_ELLIPTIC_EC = {
351
+ id: "elliptic-ec",
352
+ title: "elliptic curve instantiation",
353
+ description: "the `elliptic` library — new EC(...)",
354
+ category: "signature",
355
+ severity: "high",
356
+ confidence: "high",
357
+ algorithm: "ECDSA",
358
+ hndl: false,
359
+ cwe: CWE_BROKEN_CRYPTO,
360
+ message: "The `elliptic` library implements classical ECDSA/ECDH, both broken by Shor's algorithm.",
361
+ };
362
+ const RULE_SECP256K1 = {
363
+ id: "secp256k1-usage",
364
+ title: "secp256k1 ECDSA/ECDH usage",
365
+ description: "direct @noble/secp256k1-style API usage",
366
+ category: "signature",
367
+ severity: "high",
368
+ confidence: "medium",
369
+ algorithm: "ECDSA",
370
+ hndl: false,
371
+ cwe: CWE_BROKEN_CRYPTO,
372
+ message: "Direct secp256k1 usage (ECDSA signatures / ECDH agreement) is classical and broken by Shor's algorithm.",
373
+ remediation: "ML-DSA-65 (FIPS 204) for signatures; hybrid X25519MLKEM768 for key agreement.",
374
+ };
375
+ const RULE_JSRSASIGN_KEYGEN = {
376
+ id: "jsrsasign-keygen",
377
+ title: "jsrsasign key generation",
378
+ description: "jsrsasign KEYUTIL.generateKeypair",
379
+ category: "signature",
380
+ severity: "high",
381
+ confidence: "high",
382
+ algorithm: "unknown",
383
+ hndl: false,
384
+ cwe: CWE_BROKEN_CRYPTO,
385
+ message: "jsrsasign generates classical RSA/EC key pairs, which are not quantum-safe.",
386
+ remediation: "ML-KEM-768 (FIPS 203) / ML-DSA-65 (FIPS 204)",
387
+ };
388
+ const RULE_JSRSASIGN_SIGN = {
389
+ id: "jsrsasign-sign",
390
+ title: "jsrsasign signature",
391
+ description: "jsrsasign KJUR.crypto.Signature / ECDSA",
392
+ category: "signature",
393
+ severity: "high",
394
+ confidence: "high",
395
+ algorithm: "unknown",
396
+ hndl: false,
397
+ cwe: CWE_BROKEN_CRYPTO,
398
+ message: "jsrsasign signing uses classical RSA/ECDSA signatures, forgeable by a quantum attacker.",
399
+ remediation: "ML-DSA-65 (FIPS 204)",
400
+ };
401
+ const RULE_NODE_RSA_LIB = {
402
+ id: "node-rsa",
403
+ title: "node-rsa key/usage",
404
+ description: "the `node-rsa` library — new NodeRSA(...)",
405
+ category: "kem",
406
+ severity: "high",
407
+ confidence: "high",
408
+ algorithm: "RSA",
409
+ hndl: true,
410
+ cwe: CWE_BROKEN_CRYPTO,
411
+ message: "node-rsa wraps classical RSA encryption/signing, which is not quantum-safe.",
412
+ };
256
413
  /** Detects classical crypto from popular npm libraries used in source. */
257
414
  const libraryDetector = {
258
415
  id: "crypto-libs",
259
416
  description: "Classical asymmetric crypto via node-forge, elliptic, jsrsasign, node-rsa",
260
417
  scope: "source",
261
418
  language: "js",
419
+ rules: [
420
+ RULE_FORGE_RSA,
421
+ RULE_FORGE_ED25519,
422
+ RULE_ELLIPTIC_EC,
423
+ RULE_SECP256K1,
424
+ RULE_JSRSASIGN_KEYGEN,
425
+ RULE_JSRSASIGN_SIGN,
426
+ RULE_NODE_RSA_LIB,
427
+ ],
262
428
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
263
429
  detect({ file, content }) {
264
430
  const findings = [];
265
- const add = (re, spec) => eachMatch(re, content, (m) => findings.push(makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length })));
266
- // node-forge: pki.rsa.generateKeyPair(...)
267
- add(RE_FORGE_RSA, {
268
- ruleId: "forge-rsa-keygen",
269
- title: "node-forge RSA key generation",
270
- category: "kem",
271
- severity: "high",
272
- confidence: "high",
273
- algorithm: "RSA",
274
- hndl: true,
275
- cwe: CWE_BROKEN_CRYPTO,
276
- message: "node-forge generates a classical RSA key pair, which is not quantum-safe.",
277
- });
278
- // node-forge: forge.ed25519.* (classical EdDSA)
279
- add(RE_FORGE_ED25519, {
280
- ruleId: "forge-ed25519",
281
- title: "node-forge Ed25519 usage",
282
- category: "signature",
283
- severity: "low",
284
- confidence: "high",
285
- algorithm: "EdDSA",
286
- hndl: false,
287
- cwe: CWE_BROKEN_CRYPTO,
288
- message: "node-forge Ed25519 is a modern but still classical signature scheme.",
289
- });
290
- // elliptic: new EC('secp256k1') / new ec(...)
291
- add(RE_ELLIPTIC_EC, {
292
- ruleId: "elliptic-ec",
293
- title: "elliptic curve instantiation",
294
- category: "signature",
295
- severity: "high",
296
- confidence: "high",
297
- algorithm: "ECDSA",
298
- hndl: false,
299
- cwe: CWE_BROKEN_CRYPTO,
300
- message: "The `elliptic` library implements classical ECDSA/ECDH, both broken by Shor's algorithm.",
301
- });
302
- // Direct secp256k1 API usage: secp.sign / getPublicKey / getSharedSecret.
303
- add(RE_SECP256K1, {
304
- ruleId: "secp256k1-usage",
305
- title: "secp256k1 ECDSA/ECDH usage",
306
- category: "signature",
307
- severity: "high",
308
- confidence: "medium",
309
- algorithm: "ECDSA",
310
- hndl: false,
311
- cwe: CWE_BROKEN_CRYPTO,
312
- message: "Direct secp256k1 usage (ECDSA signatures / ECDH agreement) is classical and broken by Shor's algorithm.",
313
- remediation: "ML-DSA-65 (FIPS 204) for signatures; hybrid X25519MLKEM768 for key agreement.",
314
- });
315
- // jsrsasign: KEYUTIL.generateKeypair('RSA'|'EC', ...) or KJUR.crypto.*
316
- add(RE_JSRSASIGN_KEYGEN, {
317
- ruleId: "jsrsasign-keygen",
318
- title: "jsrsasign key generation",
319
- category: "signature",
320
- severity: "high",
321
- confidence: "high",
322
- algorithm: "unknown",
323
- hndl: false,
324
- cwe: CWE_BROKEN_CRYPTO,
325
- message: "jsrsasign generates classical RSA/EC key pairs, which are not quantum-safe.",
326
- remediation: "ML-KEM-768 (FIPS 203) / ML-DSA-65 (FIPS 204)",
327
- });
328
- add(RE_JSRSASIGN_SIGN, {
329
- ruleId: "jsrsasign-sign",
330
- title: "jsrsasign signature",
331
- category: "signature",
332
- severity: "high",
333
- confidence: "high",
334
- algorithm: "unknown",
335
- hndl: false,
336
- cwe: CWE_BROKEN_CRYPTO,
337
- message: "jsrsasign signing uses classical RSA/ECDSA signatures, forgeable by a quantum attacker.",
338
- remediation: "ML-DSA-65 (FIPS 204)",
339
- });
340
- // node-rsa: new NodeRSA(...)
341
- add(RE_NODE_RSA, {
342
- ruleId: "node-rsa",
343
- title: "node-rsa key/usage",
344
- category: "kem",
345
- severity: "high",
346
- confidence: "high",
347
- algorithm: "RSA",
348
- hndl: true,
349
- cwe: CWE_BROKEN_CRYPTO,
350
- message: "node-rsa wraps classical RSA encryption/signing, which is not quantum-safe.",
351
- });
431
+ const add = (re, rule) => eachMatch(re, content, (m) => findings.push(findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length })));
432
+ add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
433
+ add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
434
+ add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
435
+ add(RE_SECP256K1, RULE_SECP256K1); // secp.sign / getPublicKey / getSharedSecret
436
+ add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
437
+ add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
438
+ add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
352
439
  return findings;
353
440
  },
354
441
  };
355
442
  /* -------------------------------------------------------------------------- */
356
443
  /* JWT / JOSE / COSE algorithm strings */
357
444
  /* -------------------------------------------------------------------------- */
445
+ const RULE_JWT_ALG = {
446
+ id: "jwt-classical-alg",
447
+ title: "Classical JWT/JOSE algorithm",
448
+ description: "JWS alg tokens (RS/PS/ES/EdDSA)",
449
+ category: "signature",
450
+ severity: "high",
451
+ confidence: "medium",
452
+ algorithm: "unknown",
453
+ hndl: false,
454
+ cwe: CWE_BROKEN_CRYPTO,
455
+ message: "A classical JWT/JOSE signature algorithm is used, forgeable by a quantum attacker.",
456
+ remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
457
+ };
458
+ const RULE_JOSE_ECDH = {
459
+ id: "jose-ecdh-es",
460
+ title: "JOSE ECDH-ES key agreement",
461
+ description: "JOSE ECDH-ES / ECDH-ES+A*KW key agreement",
462
+ category: "key-exchange",
463
+ severity: "high",
464
+ confidence: "medium",
465
+ algorithm: "ECDH",
466
+ hndl: true,
467
+ cwe: CWE_BROKEN_CRYPTO,
468
+ message: "JOSE ECDH-ES performs classical ECDH key agreement — harvest-now-decrypt-later exposed.",
469
+ remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
470
+ };
358
471
  /**
359
472
  * Detects classical signature algorithm identifiers used by JWT/JOSE, plus
360
473
  * ECDH-ES key-agreement identifiers (HNDL-exposed). These appear as string
@@ -365,6 +478,7 @@ const jwtDetector = {
365
478
  description: "Classical JWT/JOSE algorithms (RS/PS/ES/EdDSA) and ECDH-ES key agreement",
366
479
  scope: "source",
367
480
  language: "js",
481
+ rules: [RULE_JWT_ALG, RULE_JOSE_ECDH],
368
482
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
369
483
  detect({ file, content }) {
370
484
  const findings = [];
@@ -378,40 +492,17 @@ const jwtDetector = {
378
492
  algorithm = "EdDSA";
379
493
  else
380
494
  algorithm = "ECDSA"; // ES*
381
- findings.push(makeFinding({
382
- ruleId: "jwt-classical-alg",
495
+ findings.push(findingFromRule(RULE_JWT_ALG, { file, content, index: m.index, matchLength: m[0].length }, {
383
496
  title: `JWT/JOSE algorithm ${alg}`,
384
- category: "signature",
385
- severity: "high",
386
- confidence: "medium",
387
497
  algorithm,
388
- hndl: false,
389
- cwe: CWE_BROKEN_CRYPTO,
390
498
  message: `JWT/JOSE algorithm "${alg}" is a classical signature, forgeable by a quantum attacker.`,
391
- remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
392
- file,
393
- content,
394
- index: m.index,
395
- matchLength: m[0].length,
396
499
  }));
397
500
  });
398
501
  // JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
399
502
  eachMatch(RE_JOSE_ECDH, content, (m) => {
400
- findings.push(makeFinding({
401
- ruleId: "jose-ecdh-es",
503
+ findings.push(findingFromRule(RULE_JOSE_ECDH, { file, content, index: m.index, matchLength: m[0].length }, {
402
504
  title: `JOSE key agreement ${m[1]}`,
403
- category: "key-exchange",
404
- severity: "high",
405
- confidence: "medium",
406
- algorithm: "ECDH",
407
- hndl: true,
408
- cwe: CWE_BROKEN_CRYPTO,
409
505
  message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
410
- remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
411
- file,
412
- content,
413
- index: m.index,
414
- matchLength: m[0].length,
415
506
  }));
416
507
  });
417
508
  return findings;
@@ -420,6 +511,42 @@ const jwtDetector = {
420
511
  /* -------------------------------------------------------------------------- */
421
512
  /* TLS legacy configuration */
422
513
  /* -------------------------------------------------------------------------- */
514
+ const RULE_TLS_LEGACY = {
515
+ id: "tls-legacy-version",
516
+ title: "Legacy TLS version pinned",
517
+ description: "minVersion/maxVersion/secureProtocol pinned to TLS 1.0/1.1",
518
+ category: "tls",
519
+ severity: "medium",
520
+ confidence: "high",
521
+ hndl: false,
522
+ cwe: CWE_WEAK_STRENGTH,
523
+ message: "TLS 1.0/1.1 are deprecated and insecure; require TLS 1.3.",
524
+ remediation: "Set minVersion: 'TLSv1.3' and prefer PQC-hybrid key exchange.",
525
+ };
526
+ const RULE_TLS_REJECT = {
527
+ id: "tls-reject-unauthorized",
528
+ title: "TLS certificate verification disabled",
529
+ description: "rejectUnauthorized: false",
530
+ category: "tls",
531
+ severity: "high",
532
+ confidence: "high",
533
+ hndl: false,
534
+ cwe: CWE_CERT_VALIDATION,
535
+ message: "rejectUnauthorized:false disables TLS certificate verification (MITM risk).",
536
+ remediation: "Remove rejectUnauthorized:false; verify certificates properly.",
537
+ };
538
+ const RULE_TLS_WEAK_CIPHER = {
539
+ id: "tls-weak-cipher",
540
+ title: "Weak TLS cipher configured",
541
+ description: "weak/export cipher in a `ciphers` string",
542
+ category: "tls",
543
+ severity: "medium",
544
+ confidence: "medium",
545
+ hndl: false,
546
+ cwe: CWE_WEAK_STRENGTH,
547
+ message: "A weak cipher is configured in the TLS ciphers list.",
548
+ remediation: "Use a modern AEAD cipher suite (TLS 1.3 defaults).",
549
+ };
423
550
  /**
424
551
  * Detects legacy / insecure TLS configuration expressed as JS object literals:
425
552
  * forced TLS 1.0/1.1, disabled certificate verification, and weak ciphers.
@@ -431,21 +558,13 @@ const tlsDetector = {
431
558
  description: "Legacy / insecure TLS configuration in JS objects",
432
559
  scope: "config",
433
560
  language: "js",
561
+ rules: [RULE_TLS_LEGACY, RULE_TLS_REJECT, RULE_TLS_WEAK_CIPHER],
434
562
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
435
563
  detect({ file, content }) {
436
564
  const findings = [];
437
565
  // minVersion / maxVersion / secureProtocol pinned to TLS 1.0 or 1.1.
438
566
  eachMatch(RE_TLS_LEGACY_VERSION, content, (m) => {
439
- findings.push(makeFinding({
440
- ruleId: "tls-legacy-version",
441
- title: "Legacy TLS version pinned",
442
- category: "tls",
443
- severity: "medium",
444
- confidence: "high",
445
- hndl: false,
446
- cwe: CWE_WEAK_STRENGTH,
447
- message: "TLS 1.0/1.1 are deprecated and insecure; require TLS 1.3.",
448
- remediation: "Set minVersion: 'TLSv1.3' and prefer PQC-hybrid key exchange.",
567
+ findings.push(findingFromRule(RULE_TLS_LEGACY, {
449
568
  file,
450
569
  content,
451
570
  index: m.index,
@@ -454,16 +573,7 @@ const tlsDetector = {
454
573
  });
455
574
  // rejectUnauthorized: false — disables certificate verification.
456
575
  eachMatch(RE_TLS_REJECT, content, (m) => {
457
- findings.push(makeFinding({
458
- ruleId: "tls-reject-unauthorized",
459
- title: "TLS certificate verification disabled",
460
- category: "tls",
461
- severity: "high",
462
- confidence: "high",
463
- hndl: false,
464
- cwe: CWE_CERT_VALIDATION,
465
- message: "rejectUnauthorized:false disables TLS certificate verification (MITM risk).",
466
- remediation: "Remove rejectUnauthorized:false; verify certificates properly.",
576
+ findings.push(findingFromRule(RULE_TLS_REJECT, {
467
577
  file,
468
578
  content,
469
579
  index: m.index,
@@ -472,21 +582,7 @@ const tlsDetector = {
472
582
  });
473
583
  // Weak / export ciphers referenced in a ciphers string (bounded regex).
474
584
  eachMatch(RE_TLS_WEAK_CIPHER, content, (m) => {
475
- findings.push(makeFinding({
476
- ruleId: "tls-weak-cipher",
477
- title: "Weak TLS cipher configured",
478
- category: "tls",
479
- severity: "medium",
480
- confidence: "medium",
481
- hndl: false,
482
- cwe: CWE_WEAK_STRENGTH,
483
- message: `Weak cipher (${m[1]}) configured in the TLS ciphers list.`,
484
- remediation: "Use a modern AEAD cipher suite (TLS 1.3 defaults).",
485
- file,
486
- content,
487
- index: m.index,
488
- matchLength: m[0].length,
489
- }));
585
+ findings.push(findingFromRule(RULE_TLS_WEAK_CIPHER, { file, content, index: m.index, matchLength: m[0].length }, { message: `Weak cipher (${m[1]}) configured in the TLS ciphers list.` }));
490
586
  });
491
587
  return findings;
492
588
  },
@@ -496,6 +592,33 @@ const tlsDetector = {
496
592
  /* -------------------------------------------------------------------------- */
497
593
  const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
498
594
  const RE_CERT_SIG_ALG = /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA(?:1|256|384|512)|rsassaPss|dsaWithSHA(?:1|256))\b/g;
595
+ const RULE_SSH_PUBKEY = {
596
+ id: "ssh-public-key",
597
+ title: "Classical SSH public key",
598
+ description: "ssh-rsa / ssh-ed25519 / ssh-dss / ecdsa-sha2-* public keys",
599
+ category: "certificate",
600
+ severity: "low",
601
+ confidence: "medium",
602
+ algorithm: "unknown",
603
+ hndl: false,
604
+ cwe: CWE_BROKEN_CRYPTO,
605
+ sensitive: true,
606
+ message: "A classical SSH public key is forgeable by a quantum attacker.",
607
+ remediation: "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
608
+ };
609
+ const RULE_CERT_SIG_ALG = {
610
+ id: "cert-signature-algorithm",
611
+ title: "Classical certificate signature algorithm",
612
+ description: "X.509/TLS certificate signature-algorithm identifiers",
613
+ category: "certificate",
614
+ severity: "low",
615
+ confidence: "medium",
616
+ algorithm: "unknown",
617
+ hndl: false,
618
+ cwe: CWE_BROKEN_CRYPTO,
619
+ message: "A classical certificate signature algorithm (RSA/ECDSA/DSA) is a quantum forgery surface.",
620
+ remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
621
+ };
499
622
  /**
500
623
  * Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
501
624
  * and X.509 certificate signature-algorithm identifiers in any text file. These
@@ -507,6 +630,7 @@ const sshCertDetector = {
507
630
  description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
508
631
  scope: "config",
509
632
  language: "any",
633
+ rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG],
510
634
  appliesTo: () => true,
511
635
  detect({ file, content }) {
512
636
  const findings = [];
@@ -520,22 +644,10 @@ const sshCertDetector = {
520
644
  : tok === "ssh-dss"
521
645
  ? "DSA"
522
646
  : "ECDSA";
523
- findings.push(makeFinding({
524
- ruleId: "ssh-public-key",
647
+ findings.push(findingFromRule(RULE_SSH_PUBKEY, { file, content, index: m.index, matchLength: m[0].length }, {
525
648
  title: `Classical SSH public key (${tok})`,
526
- category: "certificate",
527
- severity: "low",
528
- confidence: "medium",
529
649
  algorithm,
530
- hndl: false,
531
- cwe: CWE_BROKEN_CRYPTO,
532
- sensitive: true,
533
650
  message: `SSH public key type "${tok}" is a classical key forgeable by a quantum attacker.`,
534
- remediation: "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
535
- file,
536
- content,
537
- index: m.index,
538
- matchLength: m[0].length,
539
651
  }));
540
652
  });
541
653
  // X.509 / TLS certificate signature algorithm identifiers (forgery surface).
@@ -546,21 +658,10 @@ const sshCertDetector = {
546
658
  : tok.startsWith("ecdsa")
547
659
  ? "ECDSA"
548
660
  : "DSA";
549
- findings.push(makeFinding({
550
- ruleId: "cert-signature-algorithm",
661
+ findings.push(findingFromRule(RULE_CERT_SIG_ALG, { file, content, index: m.index, matchLength: m[0].length }, {
551
662
  title: `Classical certificate signature algorithm (${tok})`,
552
- category: "certificate",
553
- severity: "low",
554
- confidence: "medium",
555
663
  algorithm,
556
- hndl: false,
557
- cwe: CWE_BROKEN_CRYPTO,
558
664
  message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
559
- remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
560
- file,
561
- content,
562
- index: m.index,
563
- matchLength: m[0].length,
564
665
  }));
565
666
  });
566
667
  return findings;