@quantakrypto/core 0.2.2 → 0.4.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 (173) hide show
  1. package/dist/agent-types.d.ts +48 -0
  2. package/dist/agent-types.d.ts.map +1 -0
  3. package/dist/agent-types.js +2 -0
  4. package/dist/agent-types.js.map +1 -0
  5. package/dist/cache.d.ts +27 -0
  6. package/dist/cache.d.ts.map +1 -0
  7. package/dist/cache.js +93 -0
  8. package/dist/cache.js.map +1 -0
  9. package/dist/cbom.d.ts.map +1 -1
  10. package/dist/cbom.js +26 -1
  11. package/dist/cbom.js.map +1 -1
  12. package/dist/changed.d.ts.map +1 -1
  13. package/dist/changed.js +14 -2
  14. package/dist/changed.js.map +1 -1
  15. package/dist/codemods/config-toggle.d.ts +3 -0
  16. package/dist/codemods/config-toggle.d.ts.map +1 -0
  17. package/dist/codemods/config-toggle.js +26 -0
  18. package/dist/codemods/config-toggle.js.map +1 -0
  19. package/dist/codemods/registry.d.ts +26 -0
  20. package/dist/codemods/registry.d.ts.map +1 -0
  21. package/dist/codemods/registry.js +8 -0
  22. package/dist/codemods/registry.js.map +1 -0
  23. package/dist/comments.d.ts +45 -0
  24. package/dist/comments.d.ts.map +1 -0
  25. package/dist/comments.js +164 -0
  26. package/dist/comments.js.map +1 -0
  27. package/dist/config.d.ts +1 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/config.js +7 -0
  30. package/dist/config.js.map +1 -1
  31. package/dist/dependencies.d.ts +31 -13
  32. package/dist/dependencies.d.ts.map +1 -1
  33. package/dist/dependencies.js +460 -40
  34. package/dist/dependencies.js.map +1 -1
  35. package/dist/detect-utils.d.ts +70 -2
  36. package/dist/detect-utils.d.ts.map +1 -1
  37. package/dist/detect-utils.js +139 -17
  38. package/dist/detect-utils.js.map +1 -1
  39. package/dist/detectors/c.d.ts +20 -0
  40. package/dist/detectors/c.d.ts.map +1 -0
  41. package/dist/detectors/c.js +104 -0
  42. package/dist/detectors/c.js.map +1 -0
  43. package/dist/detectors/csharp.d.ts +15 -0
  44. package/dist/detectors/csharp.d.ts.map +1 -0
  45. package/dist/detectors/csharp.js +76 -0
  46. package/dist/detectors/csharp.js.map +1 -0
  47. package/dist/detectors/go.d.ts +25 -0
  48. package/dist/detectors/go.d.ts.map +1 -0
  49. package/dist/detectors/go.js +132 -0
  50. package/dist/detectors/go.js.map +1 -0
  51. package/dist/detectors/java.d.ts +25 -0
  52. package/dist/detectors/java.d.ts.map +1 -0
  53. package/dist/detectors/java.js +200 -0
  54. package/dist/detectors/java.js.map +1 -0
  55. package/dist/detectors/pem.d.ts +4 -0
  56. package/dist/detectors/pem.d.ts.map +1 -1
  57. package/dist/detectors/pem.js +113 -91
  58. package/dist/detectors/pem.js.map +1 -1
  59. package/dist/detectors/python.d.ts +26 -0
  60. package/dist/detectors/python.d.ts.map +1 -0
  61. package/dist/detectors/python.js +171 -0
  62. package/dist/detectors/python.js.map +1 -0
  63. package/dist/detectors/ruby.d.ts +15 -0
  64. package/dist/detectors/ruby.d.ts.map +1 -0
  65. package/dist/detectors/ruby.js +75 -0
  66. package/dist/detectors/ruby.js.map +1 -0
  67. package/dist/detectors/rust.d.ts +13 -0
  68. package/dist/detectors/rust.d.ts.map +1 -0
  69. package/dist/detectors/rust.js +94 -0
  70. package/dist/detectors/rust.js.map +1 -0
  71. package/dist/detectors/source.d.ts +9 -2
  72. package/dist/detectors/source.d.ts.map +1 -1
  73. package/dist/detectors/source.js +435 -286
  74. package/dist/detectors/source.js.map +1 -1
  75. package/dist/index.d.ts +19 -1
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +16 -2
  78. package/dist/index.js.map +1 -1
  79. package/dist/parallel.d.ts +4 -0
  80. package/dist/parallel.d.ts.map +1 -1
  81. package/dist/parallel.js +61 -8
  82. package/dist/parallel.js.map +1 -1
  83. package/dist/patch-policy.d.ts +22 -0
  84. package/dist/patch-policy.d.ts.map +1 -0
  85. package/dist/patch-policy.js +21 -0
  86. package/dist/patch-policy.js.map +1 -0
  87. package/dist/redact.d.ts +19 -0
  88. package/dist/redact.d.ts.map +1 -0
  89. package/dist/redact.js +77 -0
  90. package/dist/redact.js.map +1 -0
  91. package/dist/registry.d.ts +29 -4
  92. package/dist/registry.d.ts.map +1 -1
  93. package/dist/registry.js +62 -4
  94. package/dist/registry.js.map +1 -1
  95. package/dist/remediate-pipeline.d.ts +41 -0
  96. package/dist/remediate-pipeline.d.ts.map +1 -0
  97. package/dist/remediate-pipeline.js +38 -0
  98. package/dist/remediate-pipeline.js.map +1 -0
  99. package/dist/remediate-request.d.ts +21 -0
  100. package/dist/remediate-request.d.ts.map +1 -0
  101. package/dist/remediate-request.js +28 -0
  102. package/dist/remediate-request.js.map +1 -0
  103. package/dist/remediation.d.ts +10 -0
  104. package/dist/remediation.d.ts.map +1 -1
  105. package/dist/remediation.js +21 -8
  106. package/dist/remediation.js.map +1 -1
  107. package/dist/report.d.ts +9 -1
  108. package/dist/report.d.ts.map +1 -1
  109. package/dist/report.js +72 -24
  110. package/dist/report.js.map +1 -1
  111. package/dist/scan-worker.js +7 -3
  112. package/dist/scan-worker.js.map +1 -1
  113. package/dist/scan.d.ts +6 -5
  114. package/dist/scan.d.ts.map +1 -1
  115. package/dist/scan.js +59 -30
  116. package/dist/scan.js.map +1 -1
  117. package/dist/triage.d.ts +27 -0
  118. package/dist/triage.d.ts.map +1 -0
  119. package/dist/triage.js +33 -0
  120. package/dist/triage.js.map +1 -0
  121. package/dist/types.d.ts +106 -3
  122. package/dist/types.d.ts.map +1 -1
  123. package/dist/verify.d.ts +30 -0
  124. package/dist/verify.d.ts.map +1 -0
  125. package/dist/verify.js +65 -0
  126. package/dist/verify.js.map +1 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/dist/walk.d.ts +7 -0
  130. package/dist/walk.d.ts.map +1 -1
  131. package/dist/walk.js +85 -11
  132. package/dist/walk.js.map +1 -1
  133. package/dist/worktree.d.ts +6 -0
  134. package/dist/worktree.d.ts.map +1 -0
  135. package/dist/worktree.js +43 -0
  136. package/dist/worktree.js.map +1 -0
  137. package/package.json +1 -1
  138. package/src/agent-types.ts +52 -0
  139. package/src/cache.ts +120 -0
  140. package/src/cbom.ts +27 -1
  141. package/src/changed.ts +13 -4
  142. package/src/codemods/config-toggle.ts +35 -0
  143. package/src/codemods/registry.ts +31 -0
  144. package/src/comments.ts +180 -0
  145. package/src/config.ts +8 -0
  146. package/src/dependencies.ts +483 -51
  147. package/src/detect-utils.ts +180 -16
  148. package/src/detectors/c.ts +132 -0
  149. package/src/detectors/csharp.ts +98 -0
  150. package/src/detectors/go.ts +167 -0
  151. package/src/detectors/java.ts +235 -0
  152. package/src/detectors/pem.ts +124 -105
  153. package/src/detectors/python.ts +212 -0
  154. package/src/detectors/ruby.ts +97 -0
  155. package/src/detectors/rust.ts +112 -0
  156. package/src/detectors/source.ts +535 -364
  157. package/src/index.ts +47 -2
  158. package/src/parallel.ts +72 -7
  159. package/src/patch-policy.ts +42 -0
  160. package/src/redact.ts +92 -0
  161. package/src/registry.ts +71 -5
  162. package/src/remediate-pipeline.ts +81 -0
  163. package/src/remediate-request.ts +52 -0
  164. package/src/remediation.ts +27 -11
  165. package/src/report.ts +106 -24
  166. package/src/scan-worker.ts +18 -7
  167. package/src/scan.ts +74 -29
  168. package/src/triage.ts +58 -0
  169. package/src/types.ts +120 -3
  170. package/src/verify.ts +82 -0
  171. package/src/version.ts +1 -1
  172. package/src/walk.ts +81 -12
  173. package/src/worktree.ts +45 -0
@@ -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,13 @@
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,
32
+ JWT_HOST_EXTENSIONS,
25
33
  eachMatch,
34
+ findingFromRule,
26
35
  hasExtension,
27
- makeFinding,
28
36
  nearSortedCall,
29
37
  } from "../detect-utils.js";
30
38
  import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
@@ -33,31 +41,41 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
33
41
  /* Precompiled regexes (module scope — never recreated per file) */
34
42
  /* -------------------------------------------------------------------------- */
35
43
 
44
+ // `rsa-pss` is listed before `rsa` so the alternation consumes the full token
45
+ // (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
36
46
  const RE_GENERATE_KEYPAIR =
37
- /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
47
+ /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
38
48
  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). The first argument is either a quoted
42
- // digest-algorithm string (RSA/ECDSA) or `null` Node's EdDSA one-shot form is
43
- // `crypto.sign(null, data, edKey)`, which must still be detected.
49
+ // One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
50
+ // consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
51
+ // or `createSign(` (handled by the dedicated createSign/createVerify rule)
52
+ // consuming the preceding char used to push the match onto the previous line,
53
+ // corrupting the reported line/snippet and SARIF/baseline fingerprints. The
54
+ // first argument is either a quoted digest-algorithm string (RSA/ECDSA) or
55
+ // `null` — Node's EdDSA one-shot form is `crypto.sign(null, data, edKey)`.
44
56
  const RE_ONESHOT_SIGN_VERIFY =
45
- /(?:^|[^.\w])(?:crypto\.)?(sign|verify)\s*\(\s*(?:['"`][\w.-]+['"`]|null)\s*,/g;
57
+ /(?<![.\w])(?:crypto\.)?(sign|verify)\s*\(\s*(?:['"`][\w.-]+['"`]|null)\s*,/g;
46
58
  const RE_CREATE_DH = /createDiffieHellman(?:Group)?\s*\(/g;
47
59
  const RE_GET_DH = /getDiffieHellman\s*\(\s*['"`](modp\d+)['"`]\s*\)/g;
48
60
  const RE_CREATE_ECDH = /createECDH\s*\(/g;
49
61
  const RE_RSA_ENCRYPT = /(?:crypto\.)?(?:publicEncrypt|privateDecrypt)\s*\(/g;
50
62
  const RE_DH_KEYOBJECT = /(?:crypto\.)?diffieHellman\s*\(\s*\{/g;
51
63
 
52
- // WebCrypto.
53
- const RE_WEBCRYPTO_ALGO = /\b(RSA-OAEP|RSA-PSS|RSASSA-PKCS1-v1_5|ECDH|ECDSA)\b/gi;
64
+ // WebCrypto. Includes the newer curve algorithms (Ed25519/Ed448 signatures,
65
+ // X25519/X448 key agreement) shipping in modern SubtleCrypto implementations.
66
+ const RE_WEBCRYPTO_ALGO =
67
+ /\b(RSA-OAEP|RSA-PSS|RSASSA-PKCS1-v1_5|ECDH|ECDSA|Ed25519|Ed448|X25519|X448)\b/gi;
54
68
  const RE_SUBTLE_CALL =
55
69
  /subtle\s*\.\s*(generateKey|importKey|exportKey|deriveKey|deriveBits|sign|verify|encrypt|decrypt|wrapKey|unwrapKey)\s*\(/g;
56
70
 
57
71
  // Libraries.
58
72
  const RE_FORGE_RSA = /pki\.rsa\.generateKeyPair\s*\(/g;
59
73
  const RE_FORGE_ED25519 = /forge\.ed25519\b/g;
60
- const RE_ELLIPTIC_EC = /new\s+(?:elliptic\.)?ec\s*\(/gi;
74
+ // Require a curve-like first argument so `new EC("request-scope")` (a non-crypto
75
+ // `EC` class) is not flagged; the elliptic library is always constructed with a
76
+ // named curve (`new EC('secp256k1')`, `new EC('p256')`, …).
77
+ const RE_ELLIPTIC_EC =
78
+ /new\s+(?:elliptic\.)?ec\s*\(\s*['"`](?:sec[pt]|prime|nistp|curve|ed25519|ed448|brainpool|p-?(?:192|224|256|384|521)|x25519|x448)/gi;
61
79
  const RE_JSRSASIGN_KEYGEN = /KEYUTIL\.generateKeypair\s*\(/g;
62
80
  const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
63
81
  const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
@@ -84,22 +102,135 @@ const RE_TLS_WEAK_CIPHER =
84
102
  /* Node.js `crypto` module */
85
103
  /* -------------------------------------------------------------------------- */
86
104
 
105
+ /**
106
+ * Rule catalog for the Node `crypto` detector. `node-crypto-keygen` and
107
+ * `node-crypto-dh-modp` refine some fields per match; the rest emit findings
108
+ * straight from these declarations.
109
+ */
110
+ const RULE_NODE_KEYGEN: RuleMeta = {
111
+ id: "node-crypto-keygen",
112
+ title: "Classical key generation",
113
+ description: "crypto.generateKeyPair(Sync)('rsa'|'ec'|'dsa'|'dh'|…)",
114
+ category: "key-exchange",
115
+ severity: "high",
116
+ confidence: "high",
117
+ algorithm: "unknown",
118
+ hndl: true,
119
+ cwe: CWE_BROKEN_CRYPTO,
120
+ message: "Generates a classical asymmetric key pair, which is not quantum-safe.",
121
+ };
122
+ const RULE_NODE_SIGN: RuleMeta = {
123
+ id: "node-crypto-sign",
124
+ title: "Classical signature (createSign/createVerify)",
125
+ description: "crypto.createSign / crypto.createVerify",
126
+ category: "signature",
127
+ severity: "high",
128
+ confidence: "medium",
129
+ algorithm: "unknown",
130
+ hndl: false,
131
+ cwe: CWE_BROKEN_CRYPTO,
132
+ message:
133
+ "Uses createSign/createVerify, typically RSA, ECDSA or DSA — all forgeable by a quantum attacker.",
134
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
135
+ };
136
+ const RULE_NODE_SIGN_ONESHOT: RuleMeta = {
137
+ id: "node-crypto-sign-oneshot",
138
+ title: "Classical one-shot signature (crypto.sign/verify)",
139
+ description: "one-shot crypto.sign / crypto.verify (Node ≥ 12)",
140
+ category: "signature",
141
+ severity: "high",
142
+ confidence: "medium",
143
+ algorithm: "unknown",
144
+ hndl: false,
145
+ cwe: CWE_BROKEN_CRYPTO,
146
+ message:
147
+ "Uses the one-shot crypto.sign/crypto.verify API, typically RSA/ECDSA/EdDSA — forgeable by a quantum attacker.",
148
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
149
+ };
150
+ const RULE_NODE_DH: RuleMeta = {
151
+ id: "node-crypto-dh",
152
+ title: "Diffie-Hellman key exchange",
153
+ description: "crypto.createDiffieHellman(Group)",
154
+ category: "key-exchange",
155
+ severity: "high",
156
+ confidence: "high",
157
+ algorithm: "DH",
158
+ hndl: true,
159
+ cwe: CWE_BROKEN_CRYPTO,
160
+ message: "Finite-field Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
161
+ };
162
+ const RULE_NODE_DH_MODP: RuleMeta = {
163
+ id: "node-crypto-dh-modp",
164
+ title: "Diffie-Hellman MODP group",
165
+ description: "crypto.getDiffieHellman('modpN')",
166
+ category: "key-exchange",
167
+ severity: "high",
168
+ confidence: "high",
169
+ algorithm: "DH",
170
+ hndl: true,
171
+ cwe: CWE_BROKEN_CRYPTO,
172
+ message:
173
+ "Named finite-field DH MODP group is broken by Shor's algorithm (harvest-now-decrypt-later).",
174
+ };
175
+ const RULE_NODE_ECDH: RuleMeta = {
176
+ id: "node-crypto-ecdh",
177
+ title: "ECDH key exchange",
178
+ description: "crypto.createECDH",
179
+ category: "key-exchange",
180
+ severity: "high",
181
+ confidence: "high",
182
+ algorithm: "ECDH",
183
+ hndl: true,
184
+ cwe: CWE_BROKEN_CRYPTO,
185
+ message:
186
+ "Elliptic-curve Diffie-Hellman is broken by Shor's algorithm (harvest-now-decrypt-later).",
187
+ };
188
+ const RULE_NODE_RSA_ENCRYPT: RuleMeta = {
189
+ id: "node-crypto-rsa-encrypt",
190
+ title: "RSA public-key encryption",
191
+ description: "crypto.publicEncrypt / crypto.privateDecrypt",
192
+ category: "kem",
193
+ severity: "high",
194
+ confidence: "high",
195
+ algorithm: "RSA",
196
+ hndl: true,
197
+ cwe: CWE_BROKEN_CRYPTO,
198
+ message:
199
+ "RSA public-key encryption is broken by Shor's algorithm and exposed to harvest-now-decrypt-later.",
200
+ };
201
+ const RULE_NODE_DH_KEYOBJECT: RuleMeta = {
202
+ id: "node-crypto-dh-keyobject",
203
+ title: "Diffie-Hellman (KeyObject) key exchange",
204
+ description: "crypto.diffieHellman({ privateKey, publicKey })",
205
+ category: "key-exchange",
206
+ severity: "high",
207
+ confidence: "high",
208
+ algorithm: "ECDH",
209
+ hndl: true,
210
+ cwe: CWE_BROKEN_CRYPTO,
211
+ message:
212
+ "crypto.diffieHellman() performs a classical (EC)DH agreement (harvest-now-decrypt-later).",
213
+ };
214
+
87
215
  /** Detects classical asymmetric usage from Node's built-in `crypto` module. */
88
216
  const nodeCryptoDetector: Detector = {
89
217
  id: "node-crypto",
90
218
  description: "Classical asymmetric crypto via the Node.js `crypto` module",
91
219
  scope: "source",
92
220
  language: "js",
221
+ rules: [
222
+ RULE_NODE_KEYGEN,
223
+ RULE_NODE_SIGN,
224
+ RULE_NODE_SIGN_ONESHOT,
225
+ RULE_NODE_DH,
226
+ RULE_NODE_DH_MODP,
227
+ RULE_NODE_ECDH,
228
+ RULE_NODE_RSA_ENCRYPT,
229
+ RULE_NODE_DH_KEYOBJECT,
230
+ ],
93
231
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
94
232
  detect({ file, content }): Finding[] {
95
233
  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
234
 
104
235
  // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
105
236
  eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
@@ -117,6 +248,18 @@ const nodeCryptoDetector: Detector = {
117
248
  }
118
249
  > = {
119
250
  rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
251
+ // RSA-PSS is signature-only, so classify it as a (forgeable) signature
252
+ // rather than a KEM — no HNDL confidentiality exposure.
253
+ "rsa-pss": {
254
+ algo: "RSA",
255
+ cat: "signature",
256
+ sev: "high",
257
+ hndl: false,
258
+ label: "RSA-PSS",
259
+ message:
260
+ "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
261
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
262
+ },
120
263
  // EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
121
264
  // HNDL-exposed, so classify conservatively as key-exchange-capable and
122
265
  // surface both concerns rather than asserting signature-only (P0-4).
@@ -140,156 +283,103 @@ const nodeCryptoDetector: Detector = {
140
283
  ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
141
284
  };
142
285
  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,
286
+ findings.push(
287
+ findingFromRule(
288
+ RULE_NODE_KEYGEN,
289
+ { file, content, index: m.index, matchLength: m[0].length },
290
+ {
291
+ title: `${info.label} key generation`,
292
+ category: info.cat,
293
+ severity: info.sev,
294
+ algorithm: info.algo,
295
+ hndl: info.hndl,
296
+ message:
297
+ info.message ??
298
+ `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
299
+ ...(info.remediation ? { remediation: info.remediation } : {}),
300
+ },
301
+ ),
159
302
  );
160
303
  });
161
304
 
162
305
  // createSign / createVerify — RSA / ECDSA / DSA signatures.
163
306
  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,
307
+ findings.push(
308
+ findingFromRule(RULE_NODE_SIGN, {
309
+ file,
310
+ content,
311
+ index: m.index,
312
+ matchLength: m[0].length,
313
+ }),
179
314
  );
180
315
  });
181
316
 
182
317
  // One-shot crypto.sign(algorithm, data, key) / crypto.verify(...) (Node ≥ 12).
183
318
  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,
319
+ findings.push(
320
+ findingFromRule(RULE_NODE_SIGN_ONESHOT, {
321
+ file,
322
+ content,
323
+ index: m.index,
324
+ matchLength: m[0].length,
325
+ }),
199
326
  );
200
327
  });
201
328
 
202
329
  // createDiffieHellman / createDiffieHellmanGroup — finite-field DH key exchange.
203
330
  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,
331
+ findings.push(
332
+ findingFromRule(RULE_NODE_DH, { file, content, index: m.index, matchLength: m[0].length }),
218
333
  );
219
334
  });
220
335
 
221
336
  // getDiffieHellman('modpN') — named built-in finite-field MODP groups.
222
337
  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,
338
+ findings.push(
339
+ findingFromRule(
340
+ RULE_NODE_DH_MODP,
341
+ { file, content, index: m.index, matchLength: m[0].length },
342
+ {
343
+ title: `Diffie-Hellman MODP group (${m[1]})`,
344
+ message: `Named finite-field DH MODP group "${m[1]}" is broken by Shor's algorithm (harvest-now-decrypt-later).`,
345
+ },
346
+ ),
236
347
  );
237
348
  });
238
349
 
239
350
  // createECDH — elliptic-curve Diffie-Hellman key exchange.
240
351
  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,
352
+ findings.push(
353
+ findingFromRule(RULE_NODE_ECDH, {
354
+ file,
355
+ content,
356
+ index: m.index,
357
+ matchLength: m[0].length,
358
+ }),
255
359
  );
256
360
  });
257
361
 
258
362
  // publicEncrypt / privateDecrypt — RSA encryption (KEM-like confidentiality).
259
363
  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,
364
+ findings.push(
365
+ findingFromRule(RULE_NODE_RSA_ENCRYPT, {
366
+ file,
367
+ content,
368
+ index: m.index,
369
+ matchLength: m[0].length,
370
+ }),
274
371
  );
275
372
  });
276
373
 
277
374
  // diffieHellman({ privateKey, publicKey }) — KeyObject-based DH/ECDH.
278
375
  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,
376
+ findings.push(
377
+ findingFromRule(RULE_NODE_DH_KEYOBJECT, {
378
+ file,
379
+ content,
380
+ index: m.index,
381
+ matchLength: m[0].length,
382
+ }),
293
383
  );
294
384
  });
295
385
 
@@ -301,6 +391,19 @@ const nodeCryptoDetector: Detector = {
301
391
  /* WebCrypto (SubtleCrypto) */
302
392
  /* -------------------------------------------------------------------------- */
303
393
 
394
+ const RULE_WEBCRYPTO: RuleMeta = {
395
+ id: "webcrypto-classical",
396
+ title: "WebCrypto classical algorithm",
397
+ description: "classical asymmetric algorithm passed to SubtleCrypto",
398
+ category: "signature",
399
+ severity: "high",
400
+ confidence: "high",
401
+ algorithm: "unknown",
402
+ hndl: false,
403
+ cwe: CWE_BROKEN_CRYPTO,
404
+ message: "A classical asymmetric WebCrypto algorithm is used, which is not quantum-safe.",
405
+ };
406
+
304
407
  /**
305
408
  * Detects classical algorithms passed to WebCrypto's SubtleCrypto methods. The
306
409
  * algorithm name can appear as a bare string ("RSA-OAEP") or as
@@ -311,6 +414,7 @@ const webCryptoDetector: Detector = {
311
414
  description: "Classical asymmetric algorithms via WebCrypto SubtleCrypto",
312
415
  scope: "source",
313
416
  language: "js",
417
+ rules: [RULE_WEBCRYPTO],
314
418
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
315
419
  detect({ file, content }): Finding[] {
316
420
  const findings: Finding[] = [];
@@ -325,31 +429,49 @@ const webCryptoDetector: Detector = {
325
429
  eachMatch(RE_WEBCRYPTO_ALGO, content, (m) => {
326
430
  if (!nearSortedCall(callIndexes, m.index, 400)) return;
327
431
  const name = m[1].toUpperCase();
328
- const isKem = name === "RSA-OAEP";
329
- const isEcdh = name === "ECDH";
330
- const algorithm: Finding["algorithm"] = name.startsWith("RSA")
331
- ? "RSA"
332
- : isEcdh
333
- ? "ECDH"
334
- : "ECDSA";
335
- const category: Finding["category"] = isEcdh ? "key-exchange" : isKem ? "kem" : "signature";
336
- const hndl = isKem || isEcdh;
432
+ // Classify by algorithm: RSA-OAEP is KEM (HNDL); ECDH/X25519/X448 are key
433
+ // agreement (HNDL); RSA-PSS/RSASSA/ECDSA/Ed25519/Ed448 are signatures.
434
+ let algorithm: Finding["algorithm"];
435
+ let category: Finding["category"];
436
+ let hndl: boolean;
437
+ let severity: Finding["severity"] | undefined;
438
+ if (name.startsWith("RSA")) {
439
+ algorithm = "RSA";
440
+ const isKem = name === "RSA-OAEP";
441
+ category = isKem ? "kem" : "signature";
442
+ hndl = isKem;
443
+ } else if (name === "ECDH") {
444
+ algorithm = "ECDH";
445
+ category = "key-exchange";
446
+ hndl = true;
447
+ } else if (name === "X25519" || name === "X448") {
448
+ algorithm = name === "X448" ? "X448" : "X25519";
449
+ category = "key-exchange";
450
+ hndl = true;
451
+ severity = "low"; // modern but classical
452
+ } else if (name === "ED25519" || name === "ED448") {
453
+ algorithm = "EdDSA";
454
+ category = "signature";
455
+ hndl = false;
456
+ severity = "low";
457
+ } else {
458
+ algorithm = "ECDSA";
459
+ category = "signature";
460
+ hndl = false;
461
+ }
337
462
  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
- }),
463
+ findingFromRule(
464
+ RULE_WEBCRYPTO,
465
+ { file, content, index: m.index, matchLength: m[0].length },
466
+ {
467
+ title: `WebCrypto ${m[1]}`,
468
+ category,
469
+ algorithm,
470
+ hndl,
471
+ ...(severity ? { severity } : {}),
472
+ message: `WebCrypto algorithm "${m[1]}" is classical asymmetric crypto and not quantum-safe.`,
473
+ },
474
+ ),
353
475
  );
354
476
  });
355
477
 
@@ -361,119 +483,129 @@ const webCryptoDetector: Detector = {
361
483
  /* Popular crypto libraries */
362
484
  /* -------------------------------------------------------------------------- */
363
485
 
486
+ const RULE_FORGE_RSA: RuleMeta = {
487
+ id: "forge-rsa-keygen",
488
+ title: "node-forge RSA key generation",
489
+ description: "node-forge pki.rsa.generateKeyPair",
490
+ category: "kem",
491
+ severity: "high",
492
+ confidence: "high",
493
+ algorithm: "RSA",
494
+ hndl: true,
495
+ cwe: CWE_BROKEN_CRYPTO,
496
+ message: "node-forge generates a classical RSA key pair, which is not quantum-safe.",
497
+ };
498
+ const RULE_FORGE_ED25519: RuleMeta = {
499
+ id: "forge-ed25519",
500
+ title: "node-forge Ed25519 usage",
501
+ description: "node-forge forge.ed25519.*",
502
+ category: "signature",
503
+ severity: "low",
504
+ confidence: "high",
505
+ algorithm: "EdDSA",
506
+ hndl: false,
507
+ cwe: CWE_BROKEN_CRYPTO,
508
+ message: "node-forge Ed25519 is a modern but still classical signature scheme.",
509
+ };
510
+ const RULE_ELLIPTIC_EC: RuleMeta = {
511
+ id: "elliptic-ec",
512
+ title: "elliptic curve instantiation",
513
+ description: "the `elliptic` library — new EC(...)",
514
+ category: "signature",
515
+ severity: "high",
516
+ confidence: "high",
517
+ algorithm: "ECDSA",
518
+ hndl: false,
519
+ cwe: CWE_BROKEN_CRYPTO,
520
+ message:
521
+ "The `elliptic` library implements classical ECDSA/ECDH, both broken by Shor's algorithm.",
522
+ };
523
+ const RULE_SECP256K1: RuleMeta = {
524
+ id: "secp256k1-usage",
525
+ title: "secp256k1 ECDSA/ECDH usage",
526
+ description: "direct @noble/secp256k1-style API usage",
527
+ category: "signature",
528
+ severity: "high",
529
+ confidence: "medium",
530
+ algorithm: "ECDSA",
531
+ hndl: false,
532
+ cwe: CWE_BROKEN_CRYPTO,
533
+ message:
534
+ "Direct secp256k1 usage (ECDSA signatures / ECDH agreement) is classical and broken by Shor's algorithm.",
535
+ remediation: "ML-DSA-65 (FIPS 204) for signatures; hybrid X25519MLKEM768 for key agreement.",
536
+ };
537
+ const RULE_JSRSASIGN_KEYGEN: RuleMeta = {
538
+ id: "jsrsasign-keygen",
539
+ title: "jsrsasign key generation",
540
+ description: "jsrsasign KEYUTIL.generateKeypair",
541
+ category: "signature",
542
+ severity: "high",
543
+ confidence: "high",
544
+ algorithm: "unknown",
545
+ hndl: false,
546
+ cwe: CWE_BROKEN_CRYPTO,
547
+ message: "jsrsasign generates classical RSA/EC key pairs, which are not quantum-safe.",
548
+ remediation: "ML-KEM-768 (FIPS 203) / ML-DSA-65 (FIPS 204)",
549
+ };
550
+ const RULE_JSRSASIGN_SIGN: RuleMeta = {
551
+ id: "jsrsasign-sign",
552
+ title: "jsrsasign signature",
553
+ description: "jsrsasign KJUR.crypto.Signature / ECDSA",
554
+ category: "signature",
555
+ severity: "high",
556
+ confidence: "high",
557
+ algorithm: "unknown",
558
+ hndl: false,
559
+ cwe: CWE_BROKEN_CRYPTO,
560
+ message:
561
+ "jsrsasign signing uses classical RSA/ECDSA signatures, forgeable by a quantum attacker.",
562
+ remediation: "ML-DSA-65 (FIPS 204)",
563
+ };
564
+ const RULE_NODE_RSA_LIB: RuleMeta = {
565
+ id: "node-rsa",
566
+ title: "node-rsa key/usage",
567
+ description: "the `node-rsa` library — new NodeRSA(...)",
568
+ category: "kem",
569
+ severity: "high",
570
+ confidence: "high",
571
+ algorithm: "RSA",
572
+ hndl: true,
573
+ cwe: CWE_BROKEN_CRYPTO,
574
+ message: "node-rsa wraps classical RSA encryption/signing, which is not quantum-safe.",
575
+ };
576
+
364
577
  /** Detects classical crypto from popular npm libraries used in source. */
365
578
  const libraryDetector: Detector = {
366
579
  id: "crypto-libs",
367
580
  description: "Classical asymmetric crypto via node-forge, elliptic, jsrsasign, node-rsa",
368
581
  scope: "source",
369
582
  language: "js",
583
+ rules: [
584
+ RULE_FORGE_RSA,
585
+ RULE_FORGE_ED25519,
586
+ RULE_ELLIPTIC_EC,
587
+ RULE_SECP256K1,
588
+ RULE_JSRSASIGN_KEYGEN,
589
+ RULE_JSRSASIGN_SIGN,
590
+ RULE_NODE_RSA_LIB,
591
+ ],
370
592
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
371
593
  detect({ file, content }): Finding[] {
372
594
  const findings: Finding[] = [];
373
- const add = (
374
- re: RegExp,
375
- spec: Omit<Parameters<typeof makeFinding>[0], "file" | "content" | "index" | "matchLength">,
376
- ) =>
595
+ const add = (re: RegExp, rule: RuleMeta) =>
377
596
  eachMatch(re, content, (m) =>
378
597
  findings.push(
379
- makeFinding({ ...spec, file, content, index: m.index, matchLength: m[0].length }),
598
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
380
599
  ),
381
600
  );
382
601
 
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
- });
602
+ add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
603
+ add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
604
+ add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
605
+ add(RE_SECP256K1, RULE_SECP256K1); // secp.sign / getPublicKey / getSharedSecret
606
+ add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
607
+ add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
608
+ add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
477
609
 
478
610
  return findings;
479
611
  },
@@ -483,6 +615,34 @@ const libraryDetector: Detector = {
483
615
  /* JWT / JOSE / COSE algorithm strings */
484
616
  /* -------------------------------------------------------------------------- */
485
617
 
618
+ const RULE_JWT_ALG: RuleMeta = {
619
+ id: "jwt-classical-alg",
620
+ title: "Classical JWT/JOSE algorithm",
621
+ description: "JWS alg tokens (RS/PS/ES/EdDSA)",
622
+ category: "signature",
623
+ severity: "high",
624
+ confidence: "medium",
625
+ algorithm: "unknown",
626
+ hndl: false,
627
+ cwe: CWE_BROKEN_CRYPTO,
628
+ message: "A classical JWT/JOSE signature algorithm is used, forgeable by a quantum attacker.",
629
+ remediation: "ML-DSA-65 (FIPS 204); track IETF PQC JOSE/COSE algorithms",
630
+ };
631
+ const RULE_JOSE_ECDH: RuleMeta = {
632
+ id: "jose-ecdh-es",
633
+ title: "JOSE ECDH-ES key agreement",
634
+ description: "JOSE ECDH-ES / ECDH-ES+A*KW key agreement",
635
+ category: "key-exchange",
636
+ severity: "high",
637
+ confidence: "medium",
638
+ algorithm: "ECDH",
639
+ hndl: true,
640
+ cwe: CWE_BROKEN_CRYPTO,
641
+ message:
642
+ "JOSE ECDH-ES performs classical ECDH key agreement — harvest-now-decrypt-later exposed.",
643
+ remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
644
+ };
645
+
486
646
  /**
487
647
  * Detects classical signature algorithm identifiers used by JWT/JOSE, plus
488
648
  * ECDH-ES key-agreement identifiers (HNDL-exposed). These appear as string
@@ -492,8 +652,12 @@ const jwtDetector: Detector = {
492
652
  id: "jwt-jose",
493
653
  description: "Classical JWT/JOSE algorithms (RS/PS/ES/EdDSA) and ECDH-ES key agreement",
494
654
  scope: "source",
495
- language: "js",
496
- appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
655
+ // Language-agnostic evidence: a quoted "RS256"/"ES256" alg token is the same
656
+ // signal in JS/TS or Python (e.g. PyJWT `algorithm="RS256"`), so this detector
657
+ // is un-gated from JS-only to the JWT host surfaces.
658
+ language: "any",
659
+ rules: [RULE_JWT_ALG, RULE_JOSE_ECDH],
660
+ appliesTo: (f) => hasExtension(f, JWT_HOST_EXTENSIONS),
497
661
  detect({ file, content }): Finding[] {
498
662
  const findings: Finding[] = [];
499
663
 
@@ -505,45 +669,29 @@ const jwtDetector: Detector = {
505
669
  else if (alg === "EdDSA") algorithm = "EdDSA";
506
670
  else algorithm = "ECDSA"; // ES*
507
671
  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
- }),
672
+ findingFromRule(
673
+ RULE_JWT_ALG,
674
+ { file, content, index: m.index, matchLength: m[0].length },
675
+ {
676
+ title: `JWT/JOSE algorithm ${alg}`,
677
+ algorithm,
678
+ message: `JWT/JOSE algorithm "${alg}" is a classical signature, forgeable by a quantum attacker.`,
679
+ },
680
+ ),
524
681
  );
525
682
  });
526
683
 
527
684
  // JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
528
685
  eachMatch(RE_JOSE_ECDH, content, (m) => {
529
686
  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
- }),
687
+ findingFromRule(
688
+ RULE_JOSE_ECDH,
689
+ { file, content, index: m.index, matchLength: m[0].length },
690
+ {
691
+ title: `JOSE key agreement ${m[1]}`,
692
+ message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
693
+ },
694
+ ),
547
695
  );
548
696
  });
549
697
 
@@ -555,6 +703,43 @@ const jwtDetector: Detector = {
555
703
  /* TLS legacy configuration */
556
704
  /* -------------------------------------------------------------------------- */
557
705
 
706
+ const RULE_TLS_LEGACY: RuleMeta = {
707
+ id: "tls-legacy-version",
708
+ title: "Legacy TLS version pinned",
709
+ description: "minVersion/maxVersion/secureProtocol pinned to TLS 1.0/1.1",
710
+ category: "tls",
711
+ severity: "medium",
712
+ confidence: "high",
713
+ hndl: false,
714
+ cwe: CWE_WEAK_STRENGTH,
715
+ message: "TLS 1.0/1.1 are deprecated and insecure; require TLS 1.3.",
716
+ remediation: "Set minVersion: 'TLSv1.3' and prefer PQC-hybrid key exchange.",
717
+ };
718
+ const RULE_TLS_REJECT: RuleMeta = {
719
+ id: "tls-reject-unauthorized",
720
+ title: "TLS certificate verification disabled",
721
+ description: "rejectUnauthorized: false",
722
+ category: "tls",
723
+ severity: "high",
724
+ confidence: "high",
725
+ hndl: false,
726
+ cwe: CWE_CERT_VALIDATION,
727
+ message: "rejectUnauthorized:false disables TLS certificate verification (MITM risk).",
728
+ remediation: "Remove rejectUnauthorized:false; verify certificates properly.",
729
+ };
730
+ const RULE_TLS_WEAK_CIPHER: RuleMeta = {
731
+ id: "tls-weak-cipher",
732
+ title: "Weak TLS cipher configured",
733
+ description: "weak/export cipher in a `ciphers` string",
734
+ category: "tls",
735
+ severity: "medium",
736
+ confidence: "medium",
737
+ hndl: false,
738
+ cwe: CWE_WEAK_STRENGTH,
739
+ message: "A weak cipher is configured in the TLS ciphers list.",
740
+ remediation: "Use a modern AEAD cipher suite (TLS 1.3 defaults).",
741
+ };
742
+
558
743
  /**
559
744
  * Detects legacy / insecure TLS configuration expressed as JS object literals:
560
745
  * forced TLS 1.0/1.1, disabled certificate verification, and weak ciphers.
@@ -566,6 +751,7 @@ const tlsDetector: Detector = {
566
751
  description: "Legacy / insecure TLS configuration in JS objects",
567
752
  scope: "config",
568
753
  language: "js",
754
+ rules: [RULE_TLS_LEGACY, RULE_TLS_REJECT, RULE_TLS_WEAK_CIPHER],
569
755
  appliesTo: (f) => hasExtension(f, JS_TS_EXTENSIONS),
570
756
  detect({ file, content }): Finding[] {
571
757
  const findings: Finding[] = [];
@@ -573,16 +759,7 @@ const tlsDetector: Detector = {
573
759
  // minVersion / maxVersion / secureProtocol pinned to TLS 1.0 or 1.1.
574
760
  eachMatch(RE_TLS_LEGACY_VERSION, content, (m) => {
575
761
  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.",
762
+ findingFromRule(RULE_TLS_LEGACY, {
586
763
  file,
587
764
  content,
588
765
  index: m.index,
@@ -594,16 +771,7 @@ const tlsDetector: Detector = {
594
771
  // rejectUnauthorized: false — disables certificate verification.
595
772
  eachMatch(RE_TLS_REJECT, content, (m) => {
596
773
  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.",
774
+ findingFromRule(RULE_TLS_REJECT, {
607
775
  file,
608
776
  content,
609
777
  index: m.index,
@@ -615,21 +783,11 @@ const tlsDetector: Detector = {
615
783
  // Weak / export ciphers referenced in a ciphers string (bounded regex).
616
784
  eachMatch(RE_TLS_WEAK_CIPHER, content, (m) => {
617
785
  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
- }),
786
+ findingFromRule(
787
+ RULE_TLS_WEAK_CIPHER,
788
+ { file, content, index: m.index, matchLength: m[0].length },
789
+ { message: `Weak cipher (${m[1]}) configured in the TLS ciphers list.` },
790
+ ),
633
791
  );
634
792
  });
635
793
 
@@ -645,6 +803,35 @@ const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384
645
803
  const RE_CERT_SIG_ALG =
646
804
  /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA(?:1|256|384|512)|rsassaPss|dsaWithSHA(?:1|256))\b/g;
647
805
 
806
+ const RULE_SSH_PUBKEY: RuleMeta = {
807
+ id: "ssh-public-key",
808
+ title: "Classical SSH public key",
809
+ description: "ssh-rsa / ssh-ed25519 / ssh-dss / ecdsa-sha2-* public keys",
810
+ category: "certificate",
811
+ severity: "low",
812
+ confidence: "medium",
813
+ algorithm: "unknown",
814
+ hndl: false,
815
+ cwe: CWE_BROKEN_CRYPTO,
816
+ sensitive: true,
817
+ message: "A classical SSH public key is forgeable by a quantum attacker.",
818
+ remediation: "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
819
+ };
820
+ const RULE_CERT_SIG_ALG: RuleMeta = {
821
+ id: "cert-signature-algorithm",
822
+ title: "Classical certificate signature algorithm",
823
+ description: "X.509/TLS certificate signature-algorithm identifiers",
824
+ category: "certificate",
825
+ severity: "low",
826
+ confidence: "medium",
827
+ algorithm: "unknown",
828
+ hndl: false,
829
+ cwe: CWE_BROKEN_CRYPTO,
830
+ message:
831
+ "A classical certificate signature algorithm (RSA/ECDSA/DSA) is a quantum forgery surface.",
832
+ remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
833
+ };
834
+
648
835
  /**
649
836
  * Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
650
837
  * and X.509 certificate signature-algorithm identifiers in any text file. These
@@ -656,6 +843,7 @@ const sshCertDetector: Detector = {
656
843
  description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
657
844
  scope: "config",
658
845
  language: "any",
846
+ rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG],
659
847
  appliesTo: () => true,
660
848
  detect({ file, content }): Finding[] {
661
849
  const findings: Finding[] = [];
@@ -671,24 +859,15 @@ const sshCertDetector: Detector = {
671
859
  ? "DSA"
672
860
  : "ECDSA";
673
861
  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
- }),
862
+ findingFromRule(
863
+ RULE_SSH_PUBKEY,
864
+ { file, content, index: m.index, matchLength: m[0].length },
865
+ {
866
+ title: `Classical SSH public key (${tok})`,
867
+ algorithm,
868
+ message: `SSH public key type "${tok}" is a classical key forgeable by a quantum attacker.`,
869
+ },
870
+ ),
692
871
  );
693
872
  });
694
873
 
@@ -701,23 +880,15 @@ const sshCertDetector: Detector = {
701
880
  ? "ECDSA"
702
881
  : "DSA";
703
882
  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
- }),
883
+ findingFromRule(
884
+ RULE_CERT_SIG_ALG,
885
+ { file, content, index: m.index, matchLength: m[0].length },
886
+ {
887
+ title: `Classical certificate signature algorithm (${tok})`,
888
+ algorithm,
889
+ message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
890
+ },
891
+ ),
721
892
  );
722
893
  });
723
894