@quantakrypto/core 0.4.2 → 0.4.4

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 (223) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +38 -10
  3. package/dist/agent-types.js.map +1 -1
  4. package/dist/baseline.js.map +1 -1
  5. package/dist/cache.js.map +1 -1
  6. package/dist/cbom-merge.d.ts +9 -0
  7. package/dist/cbom-merge.d.ts.map +1 -0
  8. package/dist/cbom-merge.js +99 -0
  9. package/dist/cbom-merge.js.map +1 -0
  10. package/dist/cbom.js.map +1 -1
  11. package/dist/changed.js.map +1 -1
  12. package/dist/codemods/config-toggle.js.map +1 -1
  13. package/dist/codemods/registry.js.map +1 -1
  14. package/dist/comments.d.ts +29 -2
  15. package/dist/comments.d.ts.map +1 -1
  16. package/dist/comments.js +178 -18
  17. package/dist/comments.js.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/cwe.js.map +1 -1
  20. package/dist/dependencies.d.ts +3 -2
  21. package/dist/dependencies.d.ts.map +1 -1
  22. package/dist/dependencies.js +138 -2
  23. package/dist/dependencies.js.map +1 -1
  24. package/dist/detect-utils.d.ts +19 -4
  25. package/dist/detect-utils.d.ts.map +1 -1
  26. package/dist/detect-utils.js +43 -6
  27. package/dist/detect-utils.js.map +1 -1
  28. package/dist/detectors/c.d.ts +4 -2
  29. package/dist/detectors/c.d.ts.map +1 -1
  30. package/dist/detectors/c.js +382 -3
  31. package/dist/detectors/c.js.map +1 -1
  32. package/dist/detectors/cicd.d.ts +27 -0
  33. package/dist/detectors/cicd.d.ts.map +1 -0
  34. package/dist/detectors/cicd.js +116 -0
  35. package/dist/detectors/cicd.js.map +1 -0
  36. package/dist/detectors/cloud-kms.d.ts +21 -0
  37. package/dist/detectors/cloud-kms.d.ts.map +1 -0
  38. package/dist/detectors/cloud-kms.js +57 -0
  39. package/dist/detectors/cloud-kms.js.map +1 -0
  40. package/dist/detectors/cloudformation.d.ts +30 -0
  41. package/dist/detectors/cloudformation.d.ts.map +1 -0
  42. package/dist/detectors/cloudformation.js +170 -0
  43. package/dist/detectors/cloudformation.js.map +1 -0
  44. package/dist/detectors/csharp.d.ts.map +1 -1
  45. package/dist/detectors/csharp.js +141 -3
  46. package/dist/detectors/csharp.js.map +1 -1
  47. package/dist/detectors/database.d.ts +17 -0
  48. package/dist/detectors/database.d.ts.map +1 -0
  49. package/dist/detectors/database.js +60 -0
  50. package/dist/detectors/database.js.map +1 -0
  51. package/dist/detectors/dnssec.d.ts +55 -0
  52. package/dist/detectors/dnssec.d.ts.map +1 -0
  53. package/dist/detectors/dnssec.js +107 -0
  54. package/dist/detectors/dnssec.js.map +1 -0
  55. package/dist/detectors/elixir.d.ts +20 -0
  56. package/dist/detectors/elixir.d.ts.map +1 -0
  57. package/dist/detectors/elixir.js +195 -0
  58. package/dist/detectors/elixir.js.map +1 -0
  59. package/dist/detectors/go.d.ts +5 -3
  60. package/dist/detectors/go.d.ts.map +1 -1
  61. package/dist/detectors/go.js +187 -3
  62. package/dist/detectors/go.js.map +1 -1
  63. package/dist/detectors/java.d.ts.map +1 -1
  64. package/dist/detectors/java.js +169 -4
  65. package/dist/detectors/java.js.map +1 -1
  66. package/dist/detectors/jose.d.ts +20 -0
  67. package/dist/detectors/jose.d.ts.map +1 -0
  68. package/dist/detectors/jose.js +62 -0
  69. package/dist/detectors/jose.js.map +1 -0
  70. package/dist/detectors/jwk.d.ts +25 -0
  71. package/dist/detectors/jwk.d.ts.map +1 -0
  72. package/dist/detectors/jwk.js +95 -0
  73. package/dist/detectors/jwk.js.map +1 -0
  74. package/dist/detectors/k8s.d.ts +17 -0
  75. package/dist/detectors/k8s.d.ts.map +1 -0
  76. package/dist/detectors/k8s.js +85 -0
  77. package/dist/detectors/k8s.js.map +1 -0
  78. package/dist/detectors/mesh.d.ts +35 -0
  79. package/dist/detectors/mesh.d.ts.map +1 -0
  80. package/dist/detectors/mesh.js +109 -0
  81. package/dist/detectors/mesh.js.map +1 -0
  82. package/dist/detectors/messaging.d.ts +16 -0
  83. package/dist/detectors/messaging.d.ts.map +1 -0
  84. package/dist/detectors/messaging.js +72 -0
  85. package/dist/detectors/messaging.js.map +1 -0
  86. package/dist/detectors/pem.d.ts.map +1 -1
  87. package/dist/detectors/pem.js +50 -2
  88. package/dist/detectors/pem.js.map +1 -1
  89. package/dist/detectors/php.d.ts +24 -0
  90. package/dist/detectors/php.d.ts.map +1 -0
  91. package/dist/detectors/php.js +167 -0
  92. package/dist/detectors/php.js.map +1 -0
  93. package/dist/detectors/python.d.ts.map +1 -1
  94. package/dist/detectors/python.js +144 -3
  95. package/dist/detectors/python.js.map +1 -1
  96. package/dist/detectors/ruby.d.ts +9 -5
  97. package/dist/detectors/ruby.d.ts.map +1 -1
  98. package/dist/detectors/ruby.js +122 -2
  99. package/dist/detectors/ruby.js.map +1 -1
  100. package/dist/detectors/rust.d.ts +8 -3
  101. package/dist/detectors/rust.d.ts.map +1 -1
  102. package/dist/detectors/rust.js +253 -3
  103. package/dist/detectors/rust.js.map +1 -1
  104. package/dist/detectors/secrets.d.ts +21 -0
  105. package/dist/detectors/secrets.d.ts.map +1 -0
  106. package/dist/detectors/secrets.js +79 -0
  107. package/dist/detectors/secrets.js.map +1 -0
  108. package/dist/detectors/source.d.ts.map +1 -1
  109. package/dist/detectors/source.js +305 -52
  110. package/dist/detectors/source.js.map +1 -1
  111. package/dist/detectors/stateful-hbs.d.ts +22 -0
  112. package/dist/detectors/stateful-hbs.d.ts.map +1 -0
  113. package/dist/detectors/stateful-hbs.js +142 -0
  114. package/dist/detectors/stateful-hbs.js.map +1 -0
  115. package/dist/detectors/terraform.d.ts +21 -0
  116. package/dist/detectors/terraform.d.ts.map +1 -0
  117. package/dist/detectors/terraform.js +123 -0
  118. package/dist/detectors/terraform.js.map +1 -0
  119. package/dist/detectors/vpn.d.ts +22 -0
  120. package/dist/detectors/vpn.d.ts.map +1 -0
  121. package/dist/detectors/vpn.js +99 -0
  122. package/dist/detectors/vpn.js.map +1 -0
  123. package/dist/errors.js.map +1 -1
  124. package/dist/evidence.d.ts +53 -0
  125. package/dist/evidence.d.ts.map +1 -0
  126. package/dist/evidence.js +81 -0
  127. package/dist/evidence.js.map +1 -0
  128. package/dist/index.d.ts +9 -2
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +10 -2
  131. package/dist/index.js.map +1 -1
  132. package/dist/inventory.d.ts +2 -0
  133. package/dist/inventory.d.ts.map +1 -1
  134. package/dist/inventory.js +27 -1
  135. package/dist/inventory.js.map +1 -1
  136. package/dist/parallel.js.map +1 -1
  137. package/dist/patch-policy.js.map +1 -1
  138. package/dist/policy.d.ts +67 -0
  139. package/dist/policy.d.ts.map +1 -0
  140. package/dist/policy.js +115 -0
  141. package/dist/policy.js.map +1 -0
  142. package/dist/redact.d.ts.map +1 -1
  143. package/dist/redact.js +39 -0
  144. package/dist/redact.js.map +1 -1
  145. package/dist/registry.d.ts +2 -2
  146. package/dist/registry.d.ts.map +1 -1
  147. package/dist/registry.js +34 -2
  148. package/dist/registry.js.map +1 -1
  149. package/dist/remediate-pipeline.d.ts +3 -0
  150. package/dist/remediate-pipeline.d.ts.map +1 -1
  151. package/dist/remediate-pipeline.js +71 -0
  152. package/dist/remediate-pipeline.js.map +1 -1
  153. package/dist/remediate-request.js.map +1 -1
  154. package/dist/remediation.d.ts +9 -3
  155. package/dist/remediation.d.ts.map +1 -1
  156. package/dist/remediation.js +17 -5
  157. package/dist/remediation.js.map +1 -1
  158. package/dist/report.d.ts +9 -0
  159. package/dist/report.d.ts.map +1 -1
  160. package/dist/report.js +38 -0
  161. package/dist/report.js.map +1 -1
  162. package/dist/scan-worker.js.map +1 -1
  163. package/dist/scan.d.ts.map +1 -1
  164. package/dist/scan.js +8 -1
  165. package/dist/scan.js.map +1 -1
  166. package/dist/severity.js.map +1 -1
  167. package/dist/standards.d.ts +96 -0
  168. package/dist/standards.d.ts.map +1 -0
  169. package/dist/standards.js +107 -0
  170. package/dist/standards.js.map +1 -0
  171. package/dist/triage.js.map +1 -1
  172. package/dist/types.d.ts +2 -2
  173. package/dist/types.d.ts.map +1 -1
  174. package/dist/types.js.map +1 -1
  175. package/dist/verify.js.map +1 -1
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/version.js.map +1 -1
  179. package/dist/walk.js.map +1 -1
  180. package/dist/worktree.js.map +1 -1
  181. package/package.json +1 -1
  182. package/src/cbom-merge.ts +116 -0
  183. package/src/comments.ts +183 -18
  184. package/src/dependencies.ts +144 -4
  185. package/src/detect-utils.ts +47 -6
  186. package/src/detectors/c.ts +411 -5
  187. package/src/detectors/cicd.ts +158 -0
  188. package/src/detectors/cloud-kms.ts +87 -0
  189. package/src/detectors/cloudformation.ts +215 -0
  190. package/src/detectors/csharp.ts +154 -3
  191. package/src/detectors/database.ts +88 -0
  192. package/src/detectors/dnssec.ts +186 -0
  193. package/src/detectors/elixir.ts +237 -0
  194. package/src/detectors/go.ts +209 -6
  195. package/src/detectors/java.ts +191 -4
  196. package/src/detectors/jose.ts +94 -0
  197. package/src/detectors/jwk.ts +128 -0
  198. package/src/detectors/k8s.ts +112 -0
  199. package/src/detectors/mesh.ts +154 -0
  200. package/src/detectors/messaging.ts +98 -0
  201. package/src/detectors/pem.ts +54 -2
  202. package/src/detectors/php.ts +225 -0
  203. package/src/detectors/python.ts +153 -3
  204. package/src/detectors/ruby.ts +138 -7
  205. package/src/detectors/rust.ts +282 -7
  206. package/src/detectors/secrets.ts +114 -0
  207. package/src/detectors/source.ts +382 -72
  208. package/src/detectors/stateful-hbs.ts +180 -0
  209. package/src/detectors/terraform.ts +154 -0
  210. package/src/detectors/vpn.ts +131 -0
  211. package/src/evidence.ts +135 -0
  212. package/src/index.ts +15 -1
  213. package/src/inventory.ts +31 -1
  214. package/src/policy.ts +180 -0
  215. package/src/redact.ts +41 -0
  216. package/src/registry.ts +34 -2
  217. package/src/remediate-pipeline.ts +78 -0
  218. package/src/remediation.ts +23 -8
  219. package/src/report.ts +48 -2
  220. package/src/scan.ts +13 -1
  221. package/src/standards.ts +186 -0
  222. package/src/types.ts +3 -1
  223. package/src/version.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { JS_TS_EXTENSIONS, JWT_HOST_EXTENSIONS, eachMatch, findingFromRule, hasExtension, nearSortedCall, } from "../detect-utils.js";
1
+ import { JS_TS_EXTENSIONS, DOC_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) */
@@ -6,6 +6,107 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
6
6
  // `rsa-pss` is listed before `rsa` so the alternation consumes the full token
7
7
  // (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
8
8
  const RE_GENERATE_KEYPAIR = /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
9
+ const KEYGEN_INFO = {
10
+ rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
11
+ // RSA-PSS is signature-only, so classify it as a (forgeable) signature
12
+ // rather than a KEM — no HNDL confidentiality exposure.
13
+ "rsa-pss": {
14
+ algo: "RSA",
15
+ cat: "signature",
16
+ sev: "high",
17
+ hndl: false,
18
+ label: "RSA-PSS",
19
+ message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
20
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
21
+ },
22
+ // EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
23
+ // HNDL-exposed, so classify conservatively as key-exchange-capable and
24
+ // surface both concerns rather than asserting signature-only (P0-4).
25
+ ec: {
26
+ algo: "ECDH",
27
+ cat: "key-exchange",
28
+ sev: "high",
29
+ hndl: true,
30
+ label: "EC (ECDSA/ECDH)",
31
+ message: "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
32
+ "and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
33
+ remediation: "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
34
+ },
35
+ dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
36
+ dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
37
+ x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
38
+ x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
39
+ ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
40
+ ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
41
+ };
42
+ /** Emit a keygen finding for `generateKeyPair(Sync)('<type>')` at [index]. Shared
43
+ * by the direct matcher and the import-alias pass so both classify identically. */
44
+ function pushKeygenFinding(findings, rawType, file, content, index, matchLength) {
45
+ const info = KEYGEN_INFO[rawType.toLowerCase()];
46
+ if (!info)
47
+ return;
48
+ findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index, matchLength }, {
49
+ title: `${info.label} key generation`,
50
+ category: info.cat,
51
+ severity: info.sev,
52
+ algorithm: info.algo,
53
+ hndl: info.hndl,
54
+ message: info.message ??
55
+ `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
56
+ ...(info.remediation ? { remediation: info.remediation } : {}),
57
+ }));
58
+ }
59
+ /** Canonical Node-crypto call names whose import-aliases we resolve. */
60
+ const ALIASABLE = [
61
+ "generateKeyPairSync",
62
+ "generateKeyPair",
63
+ "createECDH",
64
+ "createDiffieHellman",
65
+ "createDiffieHellmanGroup",
66
+ ];
67
+ /**
68
+ * Collect import-aliases for the canonical crypto calls we resolve, so
69
+ * `import { generateKeyPairSync as gk } from 'node:crypto'` (or the CommonJS
70
+ * `const { generateKeyPairSync: gk } = require('crypto')`) is followed when `gk`
71
+ * is called later. Returns Map<canonical, alias[]>. An alias equal to its own
72
+ * canonical name is skipped (already matched directly). Precision-safe: the
73
+ * alias is only ever bound to a known crypto symbol, so a later `gk(` call is
74
+ * unambiguously that symbol.
75
+ */
76
+ function collectCryptoAliases(content) {
77
+ const out = new Map();
78
+ const add = (canonical, alias) => {
79
+ if (!alias || alias === canonical)
80
+ return;
81
+ const list = out.get(canonical) ?? [];
82
+ if (!list.includes(alias))
83
+ list.push(alias);
84
+ out.set(canonical, list);
85
+ };
86
+ // ESM named imports: `import { A as B, C as D } from '...'`.
87
+ const esm = /import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"][^'"]*['"]/g;
88
+ for (let m = esm.exec(content); m; m = esm.exec(content)) {
89
+ const spec = /([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g;
90
+ for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
91
+ if (ALIASABLE.includes(s[1]))
92
+ add(s[1], s[2]);
93
+ }
94
+ }
95
+ // CommonJS destructure-rename: `const { A: B } = require('...')`.
96
+ const cjs = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"][^'"]*['"]\s*\)/g;
97
+ for (let m = cjs.exec(content); m; m = cjs.exec(content)) {
98
+ const spec = /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)/g;
99
+ for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
100
+ if (ALIASABLE.includes(s[1]))
101
+ add(s[1], s[2]);
102
+ }
103
+ }
104
+ return out;
105
+ }
106
+ /** Escape a string for safe interpolation into a dynamically-built RegExp. */
107
+ function escapeRe(s) {
108
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
109
+ }
9
110
  const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
10
111
  // One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
11
112
  // consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
@@ -35,7 +136,7 @@ const RE_JSRSASIGN_KEYGEN = /KEYUTIL\.generateKeypair\s*\(/g;
35
136
  const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
36
137
  const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
37
138
  // secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
38
- const RE_SECP256K1 = /\b(?:secp(?:256k1)?|secp)\s*\.\s*(?:sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
139
+ const RE_SECP256K1 = /\b(?:secp(?:256k1)?|secp)\s*\.\s*(sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
39
140
  // JWT/JOSE.
40
141
  const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
41
142
  // JOSE ECDH-ES key agreement (HNDL) and COSE algorithm identifiers.
@@ -177,52 +278,41 @@ const nodeCryptoDetector = {
177
278
  const findings = [];
178
279
  // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
179
280
  eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
180
- const type = m[1].toLowerCase();
181
- const map = {
182
- rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
183
- // RSA-PSS is signature-only, so classify it as a (forgeable) signature
184
- // rather than a KEM — no HNDL confidentiality exposure.
185
- "rsa-pss": {
186
- algo: "RSA",
187
- cat: "signature",
188
- sev: "high",
189
- hndl: false,
190
- label: "RSA-PSS",
191
- message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
192
- remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
193
- },
194
- // EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
195
- // HNDL-exposed, so classify conservatively as key-exchange-capable and
196
- // surface both concerns rather than asserting signature-only (P0-4).
197
- ec: {
198
- algo: "ECDH",
199
- cat: "key-exchange",
200
- sev: "high",
201
- hndl: true,
202
- label: "EC (ECDSA/ECDH)",
203
- message: "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
204
- "and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
205
- remediation: "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
206
- },
207
- dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
208
- dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
209
- x25519: { algo: "X25519", cat: "key-exchange", sev: "low", hndl: true, label: "X25519" },
210
- x448: { algo: "X448", cat: "key-exchange", sev: "low", hndl: true, label: "X448" },
211
- ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
212
- ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
213
- };
214
- const info = map[type];
215
- findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index: m.index, matchLength: m[0].length }, {
216
- title: `${info.label} key generation`,
217
- category: info.cat,
218
- severity: info.sev,
219
- algorithm: info.algo,
220
- hndl: info.hndl,
221
- message: info.message ??
222
- `Generates a classical ${info.label} key pair, which is not quantum-safe.`,
223
- ...(info.remediation ? { remediation: info.remediation } : {}),
224
- }));
281
+ pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
225
282
  });
283
+ // Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
284
+ // the CommonJS destructure-rename) so an aliased call still detects. Only the
285
+ // keygen / ECDH / DH constructors are resolved — their classification is
286
+ // unambiguous. The alias regexes run on the ORIGINAL content, so locations
287
+ // stay exact; it fires only when the alias was explicitly bound to a known
288
+ // crypto symbol, so precision is unaffected.
289
+ const aliases = collectCryptoAliases(content);
290
+ for (const [canonical, names] of aliases) {
291
+ for (const alias of names) {
292
+ const a = escapeRe(alias);
293
+ if (canonical === "generateKeyPairSync" || canonical === "generateKeyPair") {
294
+ const re = new RegExp(`\\b${a}\\s*\\(\\s*['"\`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"\`]`, "g");
295
+ eachMatch(re, content, (m) => pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length));
296
+ }
297
+ else if (canonical === "createECDH") {
298
+ eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_ECDH, {
299
+ file,
300
+ content,
301
+ index: m.index,
302
+ matchLength: m[0].length,
303
+ })));
304
+ }
305
+ else {
306
+ // createDiffieHellman / createDiffieHellmanGroup.
307
+ eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_DH, {
308
+ file,
309
+ content,
310
+ index: m.index,
311
+ matchLength: m[0].length,
312
+ })));
313
+ }
314
+ }
315
+ }
226
316
  // createSign / createVerify — RSA / ECDSA / DSA signatures.
227
317
  eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
228
318
  findings.push(findingFromRule(RULE_NODE_SIGN, {
@@ -485,7 +575,21 @@ const libraryDetector = {
485
575
  add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
486
576
  add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
487
577
  add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
488
- add(RE_SECP256K1, RULE_SECP256K1); // secp.sign / getPublicKey / getSharedSecret
578
+ // secp256k1: classify per method getSharedSecret/ecdh are key AGREEMENT
579
+ // (ECDH, harvest-now-decrypt-later); sign/verify/getPublicKey/recover are
580
+ // ECDSA signatures (not HNDL). The old rule flagged everything as ECDSA.
581
+ eachMatch(RE_SECP256K1, content, (m) => {
582
+ const kex = m[1] === "getSharedSecret" || m[1] === "ecdh";
583
+ findings.push(findingFromRule(RULE_SECP256K1, { file, content, index: m.index, matchLength: m[0].length }, kex
584
+ ? {
585
+ title: "secp256k1 ECDH key agreement",
586
+ category: "key-exchange",
587
+ algorithm: "ECDH",
588
+ hndl: true,
589
+ message: `secp256k1 ECDH key agreement (.${m[1]}()) is classical and harvest-now-decrypt-later exposed.`,
590
+ }
591
+ : undefined));
592
+ });
489
593
  add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
490
594
  add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
491
595
  add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
@@ -495,6 +599,10 @@ const libraryDetector = {
495
599
  /* -------------------------------------------------------------------------- */
496
600
  /* JWT / JOSE / COSE algorithm strings */
497
601
  /* -------------------------------------------------------------------------- */
602
+ // JOSE RSA key-transport algorithms (JWE `alg`): quoted RSA-OAEP / RSA-OAEP-256
603
+ // /384/512 / RSA1_5. Classical RSA *encryption* (key transport) — distinct from
604
+ // the RS*/PS* *signature* tokens above, and a harvest-now-decrypt-later surface.
605
+ const RE_JOSE_KEM = /['"`](RSA-OAEP(?:-(?:256|384|512))?|RSA1_5)['"`]/g;
498
606
  const RULE_JWT_ALG = {
499
607
  id: "jwt-classical-alg",
500
608
  title: "Classical JWT/JOSE algorithm",
@@ -521,6 +629,19 @@ const RULE_JOSE_ECDH = {
521
629
  message: "JOSE ECDH-ES performs classical ECDH key agreement — harvest-now-decrypt-later exposed.",
522
630
  remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
523
631
  };
632
+ const RULE_JOSE_RSA_OAEP = {
633
+ id: "jose-rsa-oaep",
634
+ title: "JOSE RSA key-transport algorithm",
635
+ description: "JWE RSA-OAEP / RSA-OAEP-256/384/512 / RSA1_5 key encryption",
636
+ category: "key-exchange",
637
+ severity: "high",
638
+ confidence: "medium",
639
+ algorithm: "RSA",
640
+ hndl: true,
641
+ cwe: CWE_BROKEN_CRYPTO,
642
+ message: "JOSE RSA key transport (RSA-OAEP / RSA1_5) is classical RSA encryption — harvest-now-decrypt-later exposed.",
643
+ remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
644
+ };
524
645
  /**
525
646
  * Detects classical signature algorithm identifiers used by JWT/JOSE, plus
526
647
  * ECDH-ES key-agreement identifiers (HNDL-exposed). These appear as string
@@ -534,7 +655,7 @@ const jwtDetector = {
534
655
  // signal in JS/TS or Python (e.g. PyJWT `algorithm="RS256"`), so this detector
535
656
  // is un-gated from JS-only to the JWT host surfaces.
536
657
  language: "any",
537
- rules: [RULE_JWT_ALG, RULE_JOSE_ECDH],
658
+ rules: [RULE_JWT_ALG, RULE_JOSE_ECDH, RULE_JOSE_RSA_OAEP],
538
659
  appliesTo: (f) => hasExtension(f, JWT_HOST_EXTENSIONS),
539
660
  detect({ file, content }) {
540
661
  const findings = [];
@@ -561,6 +682,13 @@ const jwtDetector = {
561
682
  message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
562
683
  }));
563
684
  });
685
+ // JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
686
+ eachMatch(RE_JOSE_KEM, content, (m) => {
687
+ findings.push(findingFromRule(RULE_JOSE_RSA_OAEP, { file, content, index: m.index, matchLength: m[0].length }, {
688
+ title: `JOSE RSA key transport ${m[1]}`,
689
+ message: `JOSE "${m[1]}" is classical RSA key transport — harvest-now-decrypt-later exposed.`,
690
+ }));
691
+ });
564
692
  return findings;
565
693
  },
566
694
  };
@@ -648,6 +776,47 @@ const tlsDetector = {
648
776
  /* -------------------------------------------------------------------------- */
649
777
  const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
650
778
  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;
779
+ // Classical SSH key-exchange algorithm identifiers: finite-field DH groups
780
+ // (diffie-hellman-group{1,14,15,16,17,18} + group-exchange), ECDH over NIST
781
+ // curves (ecdh-sha2-nistp*), and Curve25519 (curve25519-sha256). All are
782
+ // Shor-broken key agreement — the harvest-now-decrypt-later surface a lexical
783
+ // scan misses when only the `crypto/*` API is matched, not the negotiated kex.
784
+ const RE_SSH_KEX = /\b(diffie-hellman-group(?:1|14|15|16|17|18)(?:-sha1|-sha256|-sha512)?|diffie-hellman-group-exchange-sha(?:1|256)|ecdh-sha2-nistp(?:256|384|521)|curve25519-sha256)\b/g;
785
+ // A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
786
+ // when it is EITHER (a) followed by base64 key material — an actual
787
+ // authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
788
+ // algorithm tokens on the same line — an algorithm-preference list
789
+ // (`HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,ssh-rsa`, `%w[…]`, …). A
790
+ // lone token, e.g. a UI label / i18n value like `"ssh-rsa": "ssh-rsa"`, is
791
+ // neither, so it must NOT be flagged. `RE_SSH_ALGO_TOKEN` is a separate (cloned-
792
+ // on-use) copy so counting per line never disturbs the outer `eachMatch` state.
793
+ const RE_SSH_ALGO_TOKEN = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
794
+ /** How far around a token we look for a sibling algorithm on the same line. A
795
+ * real `HostKeyAlgorithms` list is short; bounding the window keeps the per-token
796
+ * work O(1) so a pathological single huge line can't make detection quadratic. */
797
+ const SSH_LINE_WINDOW = 512;
798
+ function isRealSshKeyOrAlgoList(content, index, matchLen) {
799
+ // (a) base64 key material immediately after the token → a real SSH public key.
800
+ if (/^\s+[A-Za-z0-9+/]{20,}/.test(content.slice(index + matchLen, index + matchLen + 80))) {
801
+ return true;
802
+ }
803
+ // (b) ≥2 distinct ssh key/host-key algorithm tokens on the same line → a list.
804
+ // Bound both the back-scan for the line start and the window itself so the work
805
+ // is constant per token and never crosses a line boundary (i18n entries live on
806
+ // separate lines, so a lone repeated token stays a single distinct token).
807
+ const from = Math.max(0, index - SSH_LINE_WINDOW);
808
+ const pre = content.slice(from, index);
809
+ const preNl = pre.lastIndexOf("\n");
810
+ const lineStart = preNl === -1 ? from : from + preNl + 1;
811
+ const to = Math.min(content.length, index + matchLen + SSH_LINE_WINDOW);
812
+ const post = content.slice(index, to);
813
+ const postNl = post.indexOf("\n");
814
+ const lineEnd = postNl === -1 ? to : index + postNl;
815
+ const distinct = new Set();
816
+ for (const t of content.slice(lineStart, lineEnd).matchAll(RE_SSH_ALGO_TOKEN))
817
+ distinct.add(t[1]);
818
+ return distinct.size >= 2;
819
+ }
651
820
  const RULE_SSH_PUBKEY = {
652
821
  id: "ssh-public-key",
653
822
  title: "Classical SSH public key",
@@ -660,7 +829,7 @@ const RULE_SSH_PUBKEY = {
660
829
  cwe: CWE_BROKEN_CRYPTO,
661
830
  sensitive: true,
662
831
  message: "A classical SSH public key is forgeable by a quantum attacker.",
663
- remediation: "Plan migration to PQC-capable SSH (e.g. sntrup761x25519 KEX, PQC host keys).",
832
+ remediation: "Plan migration to PQC-capable SSH: prefer the mlkem768x25519-sha256 KEX (ML-KEM-768 hybrid, OpenSSH 10's default since Apr 2025); sntrup761x25519 is an acceptable interim. Rotate to PQC host keys as they land.",
664
833
  };
665
834
  const RULE_CERT_SIG_ALG = {
666
835
  id: "cert-signature-algorithm",
@@ -675,6 +844,19 @@ const RULE_CERT_SIG_ALG = {
675
844
  message: "A classical certificate signature algorithm (RSA/ECDSA/DSA) is a quantum forgery surface.",
676
845
  remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
677
846
  };
847
+ const RULE_SSH_KEX = {
848
+ id: "ssh-kex-classical",
849
+ title: "Classical SSH key exchange",
850
+ description: "diffie-hellman-group* / group-exchange / ecdh-sha2-* / curve25519-sha256 kex",
851
+ category: "key-exchange",
852
+ severity: "medium",
853
+ confidence: "medium",
854
+ algorithm: "unknown",
855
+ hndl: true,
856
+ cwe: CWE_BROKEN_CRYPTO,
857
+ message: "A classical SSH key-exchange algorithm (finite-field DH / ECDH / X25519) is harvest-now-decrypt-later exposed.",
858
+ remediation: "Prefer the mlkem768x25519-sha256 KEX (ML-KEM-768 hybrid, OpenSSH 10 default); sntrup761x25519 is an acceptable interim.",
859
+ };
678
860
  /**
679
861
  * Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
680
862
  * and X.509 certificate signature-algorithm identifiers in any text file. These
@@ -686,12 +868,18 @@ const sshCertDetector = {
686
868
  description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
687
869
  scope: "config",
688
870
  language: "any",
689
- rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG],
690
- appliesTo: () => true,
871
+ rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG, RULE_SSH_KEX],
872
+ // Skip prose/docs: a changelog or README that merely mentions `ssh-rsa` in a
873
+ // sentence is not crypto config. PEM material is caught by its own detector.
874
+ appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
691
875
  detect({ file, content }) {
692
876
  const findings = [];
693
877
  // SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
694
878
  eachMatch(RE_SSH_PUBKEY, content, (m) => {
879
+ // Skip a bare token that is neither a real key nor an algorithm list (e.g.
880
+ // a `"ssh-rsa": "ssh-rsa"` i18n label) — see `isRealSshKeyOrAlgoList`.
881
+ if (!isRealSshKeyOrAlgoList(content, m.index, m[0].length))
882
+ return;
695
883
  const tok = m[1];
696
884
  const algorithm = tok.startsWith("ssh-rsa")
697
885
  ? "RSA"
@@ -720,6 +908,70 @@ const sshCertDetector = {
720
908
  message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
721
909
  }));
722
910
  });
911
+ // SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
912
+ eachMatch(RE_SSH_KEX, content, (m) => {
913
+ const tok = m[1];
914
+ const algorithm = tok.startsWith("diffie-hellman")
915
+ ? "DH"
916
+ : tok.startsWith("ecdh")
917
+ ? "ECDH"
918
+ : "X25519";
919
+ findings.push(findingFromRule(RULE_SSH_KEX, { file, content, index: m.index, matchLength: m[0].length }, {
920
+ title: `Classical SSH key exchange (${tok})`,
921
+ algorithm,
922
+ message: `SSH key-exchange "${tok}" is classical (${algorithm}) — harvest-now-decrypt-later exposed.`,
923
+ }));
924
+ });
925
+ return findings;
926
+ },
927
+ };
928
+ /* -------------------------------------------------------------------------- */
929
+ /* Classical TLS key-exchange cipher suites (language-agnostic config) */
930
+ /* -------------------------------------------------------------------------- */
931
+ // Classical KEX cipher-suite identifiers, in both the OpenSSL (`ECDHE-RSA-…`) and
932
+ // IANA (`TLS_ECDHE_RSA_WITH_…`) spellings, with `-` or `_` separators. These name
933
+ // a Shor-broken key exchange (ECDHE/DHE), so they are the harvest-now surface a
934
+ // PQC scanner should flag — distinct from the legacy-*version* / weak-cipher rules
935
+ // above. Case-sensitive on purpose: cipher-suite tokens are upper-case constants,
936
+ // so prose like "ecdhe rsa" does not misfire. No trailing \b so the IANA
937
+ // `TLS_ECDHE_RSA_WITH_…` form (a `_` follows `RSA`) still matches.
938
+ const RE_TLS_CLASSICAL_KEX = /\b(?:TLS_)?(?:ECDHE|ECDH|DHE)[-_](?:RSA|ECDSA|DSS)/g;
939
+ const RULE_TLS_CLASSICAL_KEX = {
940
+ id: "tls-classical-kex",
941
+ title: "Classical TLS key-exchange cipher suite",
942
+ description: "ECDHE / DHE cipher suites negotiate Shor-broken key exchange",
943
+ category: "tls",
944
+ severity: "medium",
945
+ confidence: "medium",
946
+ algorithm: "unknown",
947
+ hndl: true,
948
+ cwe: CWE_BROKEN_CRYPTO,
949
+ message: "Classical TLS key exchange (ECDHE/DHE) is harvest-now-decrypt-later exposed — the session key can be recorded now and recovered by a quantum attacker.",
950
+ remediation: "Adopt a PQC-hybrid TLS 1.3 key exchange (e.g. X25519MLKEM768) as your stack and peers support it; keep classical suites only as a transitional fallback.",
951
+ };
952
+ /**
953
+ * Detects classical TLS key-exchange cipher suites in any text/config file
954
+ * (OpenSSL cipher lists, IANA suite constants in Go/Java/C#/Rust, `.properties`,
955
+ * etc.). Language-agnostic because the suite identifiers are the same tokens
956
+ * everywhere — this is the cross-language TLS gap the legacy-version rule misses.
957
+ */
958
+ const tlsClassicalKexDetector = {
959
+ id: "tls-cipher-suite",
960
+ description: "Classical TLS key-exchange cipher suites (ECDHE/DHE) in any config",
961
+ scope: "config",
962
+ language: "any",
963
+ rules: [RULE_TLS_CLASSICAL_KEX],
964
+ appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
965
+ detect({ file, content }) {
966
+ const findings = [];
967
+ eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
968
+ const tok = m[0];
969
+ const algorithm = tok.includes("ECDH") ? "ECDH" : "DH";
970
+ findings.push(findingFromRule(RULE_TLS_CLASSICAL_KEX, { file, content, index: m.index, matchLength: m[0].length }, {
971
+ algorithm,
972
+ message: `Classical TLS key-exchange suite "${tok}…" (${algorithm}) is harvest-now-decrypt-later exposed.`,
973
+ }));
974
+ });
723
975
  return findings;
724
976
  },
725
977
  };
@@ -731,5 +983,6 @@ export const sourceDetectors = [
731
983
  jwtDetector,
732
984
  tlsDetector,
733
985
  sshCertDetector,
986
+ tlsClassicalKexDetector,
734
987
  ];
735
988
  //# sourceMappingURL=source.js.map