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