@quantakrypto/core 0.4.3 → 0.5.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 (314) hide show
  1. package/README.md +76 -27
  2. package/dist/cbom-merge.d.ts +9 -0
  3. package/dist/cbom-merge.d.ts.map +1 -0
  4. package/dist/cbom-merge.js +121 -0
  5. package/dist/cbom-merge.js.map +1 -0
  6. package/dist/cbom.d.ts +5 -0
  7. package/dist/cbom.d.ts.map +1 -1
  8. package/dist/cbom.js +132 -37
  9. package/dist/cbom.js.map +1 -1
  10. package/dist/comments.d.ts +6 -26
  11. package/dist/comments.d.ts.map +1 -1
  12. package/dist/comments.js +77 -38
  13. package/dist/comments.js.map +1 -1
  14. package/dist/dependencies.d.ts.map +1 -1
  15. package/dist/dependencies.js +1 -1
  16. package/dist/dependencies.js.map +1 -1
  17. package/dist/detect-utils.d.ts +59 -21
  18. package/dist/detect-utils.d.ts.map +1 -1
  19. package/dist/detect-utils.js +143 -8
  20. package/dist/detect-utils.js.map +1 -1
  21. package/dist/detectors/age.d.ts +16 -0
  22. package/dist/detectors/age.d.ts.map +1 -0
  23. package/dist/detectors/age.js +40 -0
  24. package/dist/detectors/age.js.map +1 -0
  25. package/dist/detectors/ansible.d.ts +13 -0
  26. package/dist/detectors/ansible.d.ts.map +1 -0
  27. package/dist/detectors/ansible.js +108 -0
  28. package/dist/detectors/ansible.js.map +1 -0
  29. package/dist/detectors/bicep.d.ts +20 -0
  30. package/dist/detectors/bicep.d.ts.map +1 -0
  31. package/dist/detectors/bicep.js +74 -0
  32. package/dist/detectors/bicep.js.map +1 -0
  33. package/dist/detectors/c.d.ts +4 -2
  34. package/dist/detectors/c.d.ts.map +1 -1
  35. package/dist/detectors/c.js +222 -4
  36. package/dist/detectors/c.js.map +1 -1
  37. package/dist/detectors/cicd.d.ts +27 -0
  38. package/dist/detectors/cicd.d.ts.map +1 -0
  39. package/dist/detectors/cicd.js +129 -0
  40. package/dist/detectors/cicd.js.map +1 -0
  41. package/dist/detectors/cloud-kms.d.ts +24 -0
  42. package/dist/detectors/cloud-kms.d.ts.map +1 -0
  43. package/dist/detectors/cloud-kms.js +99 -0
  44. package/dist/detectors/cloud-kms.js.map +1 -0
  45. package/dist/detectors/cloudformation.d.ts +38 -0
  46. package/dist/detectors/cloudformation.d.ts.map +1 -0
  47. package/dist/detectors/cloudformation.js +201 -0
  48. package/dist/detectors/cloudformation.js.map +1 -0
  49. package/dist/detectors/codesign.d.ts +50 -0
  50. package/dist/detectors/codesign.d.ts.map +1 -0
  51. package/dist/detectors/codesign.js +143 -0
  52. package/dist/detectors/codesign.js.map +1 -0
  53. package/dist/detectors/csharp.d.ts.map +1 -1
  54. package/dist/detectors/csharp.js +13 -3
  55. package/dist/detectors/csharp.js.map +1 -1
  56. package/dist/detectors/dart.d.ts +35 -0
  57. package/dist/detectors/dart.d.ts.map +1 -0
  58. package/dist/detectors/dart.js +177 -0
  59. package/dist/detectors/dart.js.map +1 -0
  60. package/dist/detectors/database.d.ts +19 -0
  61. package/dist/detectors/database.d.ts.map +1 -0
  62. package/dist/detectors/database.js +97 -0
  63. package/dist/detectors/database.js.map +1 -0
  64. package/dist/detectors/dkim.d.ts +54 -0
  65. package/dist/detectors/dkim.d.ts.map +1 -0
  66. package/dist/detectors/dkim.js +92 -0
  67. package/dist/detectors/dkim.js.map +1 -0
  68. package/dist/detectors/dnssec.d.ts +55 -0
  69. package/dist/detectors/dnssec.d.ts.map +1 -0
  70. package/dist/detectors/dnssec.js +116 -0
  71. package/dist/detectors/dnssec.js.map +1 -0
  72. package/dist/detectors/elixir.d.ts +20 -0
  73. package/dist/detectors/elixir.d.ts.map +1 -0
  74. package/dist/detectors/elixir.js +227 -0
  75. package/dist/detectors/elixir.js.map +1 -0
  76. package/dist/detectors/java.d.ts.map +1 -1
  77. package/dist/detectors/java.js +34 -4
  78. package/dist/detectors/java.js.map +1 -1
  79. package/dist/detectors/jose.d.ts +20 -0
  80. package/dist/detectors/jose.d.ts.map +1 -0
  81. package/dist/detectors/jose.js +72 -0
  82. package/dist/detectors/jose.js.map +1 -0
  83. package/dist/detectors/jwk.d.ts +25 -0
  84. package/dist/detectors/jwk.d.ts.map +1 -0
  85. package/dist/detectors/jwk.js +131 -0
  86. package/dist/detectors/jwk.js.map +1 -0
  87. package/dist/detectors/k8s.d.ts +17 -0
  88. package/dist/detectors/k8s.d.ts.map +1 -0
  89. package/dist/detectors/k8s.js +88 -0
  90. package/dist/detectors/k8s.js.map +1 -0
  91. package/dist/detectors/keystore.d.ts +19 -0
  92. package/dist/detectors/keystore.d.ts.map +1 -0
  93. package/dist/detectors/keystore.js +76 -0
  94. package/dist/detectors/keystore.js.map +1 -0
  95. package/dist/detectors/mesh.d.ts +34 -0
  96. package/dist/detectors/mesh.d.ts.map +1 -0
  97. package/dist/detectors/mesh.js +91 -0
  98. package/dist/detectors/mesh.js.map +1 -0
  99. package/dist/detectors/messaging.d.ts +16 -0
  100. package/dist/detectors/messaging.d.ts.map +1 -0
  101. package/dist/detectors/messaging.js +79 -0
  102. package/dist/detectors/messaging.js.map +1 -0
  103. package/dist/detectors/objc.d.ts +41 -0
  104. package/dist/detectors/objc.d.ts.map +1 -0
  105. package/dist/detectors/objc.js +146 -0
  106. package/dist/detectors/objc.js.map +1 -0
  107. package/dist/detectors/openpgp.d.ts +20 -0
  108. package/dist/detectors/openpgp.d.ts.map +1 -0
  109. package/dist/detectors/openpgp.js +151 -0
  110. package/dist/detectors/openpgp.js.map +1 -0
  111. package/dist/detectors/pem.d.ts.map +1 -1
  112. package/dist/detectors/pem.js +33 -1
  113. package/dist/detectors/pem.js.map +1 -1
  114. package/dist/detectors/php.d.ts +25 -0
  115. package/dist/detectors/php.d.ts.map +1 -0
  116. package/dist/detectors/php.js +171 -0
  117. package/dist/detectors/php.js.map +1 -0
  118. package/dist/detectors/pkcs11.d.ts +17 -0
  119. package/dist/detectors/pkcs11.d.ts.map +1 -0
  120. package/dist/detectors/pkcs11.js +87 -0
  121. package/dist/detectors/pkcs11.js.map +1 -0
  122. package/dist/detectors/proxy.d.ts +52 -0
  123. package/dist/detectors/proxy.d.ts.map +1 -0
  124. package/dist/detectors/proxy.js +157 -0
  125. package/dist/detectors/proxy.js.map +1 -0
  126. package/dist/detectors/pulumi.d.ts +19 -0
  127. package/dist/detectors/pulumi.d.ts.map +1 -0
  128. package/dist/detectors/pulumi.js +80 -0
  129. package/dist/detectors/pulumi.js.map +1 -0
  130. package/dist/detectors/python.d.ts.map +1 -1
  131. package/dist/detectors/python.js +70 -0
  132. package/dist/detectors/python.js.map +1 -1
  133. package/dist/detectors/rust.d.ts.map +1 -1
  134. package/dist/detectors/rust.js +73 -0
  135. package/dist/detectors/rust.js.map +1 -1
  136. package/dist/detectors/secrets.d.ts +24 -0
  137. package/dist/detectors/secrets.d.ts.map +1 -0
  138. package/dist/detectors/secrets.js +63 -0
  139. package/dist/detectors/secrets.js.map +1 -0
  140. package/dist/detectors/solidity.d.ts +50 -0
  141. package/dist/detectors/solidity.d.ts.map +1 -0
  142. package/dist/detectors/solidity.js +108 -0
  143. package/dist/detectors/solidity.js.map +1 -0
  144. package/dist/detectors/source.d.ts.map +1 -1
  145. package/dist/detectors/source.js +243 -53
  146. package/dist/detectors/source.js.map +1 -1
  147. package/dist/detectors/spire.d.ts +49 -0
  148. package/dist/detectors/spire.d.ts.map +1 -0
  149. package/dist/detectors/spire.js +77 -0
  150. package/dist/detectors/spire.js.map +1 -0
  151. package/dist/detectors/ssh-ca.d.ts +72 -0
  152. package/dist/detectors/ssh-ca.d.ts.map +1 -0
  153. package/dist/detectors/ssh-ca.js +119 -0
  154. package/dist/detectors/ssh-ca.js.map +1 -0
  155. package/dist/detectors/stateful-hbs.d.ts.map +1 -1
  156. package/dist/detectors/stateful-hbs.js +9 -6
  157. package/dist/detectors/stateful-hbs.js.map +1 -1
  158. package/dist/detectors/supply-chain.d.ts +17 -0
  159. package/dist/detectors/supply-chain.d.ts.map +1 -0
  160. package/dist/detectors/supply-chain.js +89 -0
  161. package/dist/detectors/supply-chain.js.map +1 -0
  162. package/dist/detectors/swift.d.ts +22 -0
  163. package/dist/detectors/swift.d.ts.map +1 -0
  164. package/dist/detectors/swift.js +120 -0
  165. package/dist/detectors/swift.js.map +1 -0
  166. package/dist/detectors/terraform.d.ts +21 -0
  167. package/dist/detectors/terraform.d.ts.map +1 -0
  168. package/dist/detectors/terraform.js +150 -0
  169. package/dist/detectors/terraform.js.map +1 -0
  170. package/dist/detectors/vault.d.ts +15 -0
  171. package/dist/detectors/vault.d.ts.map +1 -0
  172. package/dist/detectors/vault.js +97 -0
  173. package/dist/detectors/vault.js.map +1 -0
  174. package/dist/detectors/vpn.d.ts +22 -0
  175. package/dist/detectors/vpn.d.ts.map +1 -0
  176. package/dist/detectors/vpn.js +73 -0
  177. package/dist/detectors/vpn.js.map +1 -0
  178. package/dist/detectors/weak-hash.d.ts +71 -0
  179. package/dist/detectors/weak-hash.d.ts.map +1 -0
  180. package/dist/detectors/weak-hash.js +116 -0
  181. package/dist/detectors/weak-hash.js.map +1 -0
  182. package/dist/detectors/webauthn.d.ts +61 -0
  183. package/dist/detectors/webauthn.d.ts.map +1 -0
  184. package/dist/detectors/webauthn.js +109 -0
  185. package/dist/detectors/webauthn.js.map +1 -0
  186. package/dist/detectors/xmldsig.d.ts +23 -0
  187. package/dist/detectors/xmldsig.d.ts.map +1 -0
  188. package/dist/detectors/xmldsig.js +84 -0
  189. package/dist/detectors/xmldsig.js.map +1 -0
  190. package/dist/evidence.d.ts +79 -4
  191. package/dist/evidence.d.ts.map +1 -1
  192. package/dist/evidence.js +76 -0
  193. package/dist/evidence.js.map +1 -1
  194. package/dist/index.d.ts +17 -9
  195. package/dist/index.d.ts.map +1 -1
  196. package/dist/index.js +21 -9
  197. package/dist/index.js.map +1 -1
  198. package/dist/inventory.d.ts +0 -2
  199. package/dist/inventory.d.ts.map +1 -1
  200. package/dist/inventory.js +15 -6
  201. package/dist/inventory.js.map +1 -1
  202. package/dist/policy.d.ts +67 -0
  203. package/dist/policy.d.ts.map +1 -0
  204. package/dist/policy.js +115 -0
  205. package/dist/policy.js.map +1 -0
  206. package/dist/registry.d.ts +9 -4
  207. package/dist/registry.d.ts.map +1 -1
  208. package/dist/registry.js +81 -4
  209. package/dist/registry.js.map +1 -1
  210. package/dist/remediate-pipeline.d.ts +0 -3
  211. package/dist/remediate-pipeline.d.ts.map +1 -1
  212. package/dist/remediate-pipeline.js +23 -2
  213. package/dist/remediate-pipeline.js.map +1 -1
  214. package/dist/remediation.d.ts +10 -0
  215. package/dist/remediation.d.ts.map +1 -1
  216. package/dist/remediation.js +32 -0
  217. package/dist/remediation.js.map +1 -1
  218. package/dist/report.d.ts +9 -0
  219. package/dist/report.d.ts.map +1 -1
  220. package/dist/report.js +38 -1
  221. package/dist/report.js.map +1 -1
  222. package/dist/scan-worker.js +5 -3
  223. package/dist/scan-worker.js.map +1 -1
  224. package/dist/scan.d.ts.map +1 -1
  225. package/dist/scan.js +35 -6
  226. package/dist/scan.js.map +1 -1
  227. package/dist/standards-profiles.d.ts +58 -0
  228. package/dist/standards-profiles.d.ts.map +1 -0
  229. package/dist/standards-profiles.js +100 -0
  230. package/dist/standards-profiles.js.map +1 -0
  231. package/dist/standards.d.ts +96 -0
  232. package/dist/standards.d.ts.map +1 -0
  233. package/dist/standards.js +107 -0
  234. package/dist/standards.js.map +1 -0
  235. package/dist/types.d.ts +6 -5
  236. package/dist/types.d.ts.map +1 -1
  237. package/dist/types.js.map +1 -1
  238. package/dist/version.d.ts +1 -1
  239. package/dist/version.js +1 -1
  240. package/dist/version.js.map +1 -1
  241. package/dist/vex.d.ts +39 -0
  242. package/dist/vex.d.ts.map +1 -0
  243. package/dist/vex.js +87 -0
  244. package/dist/vex.js.map +1 -0
  245. package/dist/walk.d.ts +8 -3
  246. package/dist/walk.d.ts.map +1 -1
  247. package/dist/walk.js +32 -1
  248. package/dist/walk.js.map +1 -1
  249. package/package.json +1 -1
  250. package/src/cbom-merge.ts +146 -0
  251. package/src/cbom.ts +153 -39
  252. package/src/comments.ts +84 -39
  253. package/src/dependencies.ts +2 -1
  254. package/src/detect-utils.ts +146 -12
  255. package/src/detectors/age.ts +60 -0
  256. package/src/detectors/ansible.ts +140 -0
  257. package/src/detectors/bicep.ts +107 -0
  258. package/src/detectors/c.ts +240 -6
  259. package/src/detectors/cicd.ts +171 -0
  260. package/src/detectors/cloud-kms.ts +141 -0
  261. package/src/detectors/cloudformation.ts +250 -0
  262. package/src/detectors/codesign.ts +214 -0
  263. package/src/detectors/csharp.ts +13 -3
  264. package/src/detectors/dart.ts +237 -0
  265. package/src/detectors/database.ts +141 -0
  266. package/src/detectors/dkim.ts +172 -0
  267. package/src/detectors/dnssec.ts +195 -0
  268. package/src/detectors/elixir.ts +268 -0
  269. package/src/detectors/java.ts +39 -4
  270. package/src/detectors/jose.ts +110 -0
  271. package/src/detectors/jwk.ts +170 -0
  272. package/src/detectors/k8s.ts +115 -0
  273. package/src/detectors/keystore.ts +109 -0
  274. package/src/detectors/mesh.ts +130 -0
  275. package/src/detectors/messaging.ts +106 -0
  276. package/src/detectors/objc.ts +209 -0
  277. package/src/detectors/openpgp.ts +194 -0
  278. package/src/detectors/pem.ts +31 -1
  279. package/src/detectors/php.ts +230 -0
  280. package/src/detectors/pkcs11.ts +113 -0
  281. package/src/detectors/proxy.ts +236 -0
  282. package/src/detectors/pulumi.ts +106 -0
  283. package/src/detectors/python.ts +71 -0
  284. package/src/detectors/rust.ts +76 -0
  285. package/src/detectors/secrets.ts +103 -0
  286. package/src/detectors/solidity.ts +176 -0
  287. package/src/detectors/source.ts +283 -73
  288. package/src/detectors/spire.ts +143 -0
  289. package/src/detectors/ssh-ca.ts +215 -0
  290. package/src/detectors/stateful-hbs.ts +9 -6
  291. package/src/detectors/supply-chain.ts +117 -0
  292. package/src/detectors/swift.ts +149 -0
  293. package/src/detectors/terraform.ts +188 -0
  294. package/src/detectors/vault.ts +120 -0
  295. package/src/detectors/vpn.ts +103 -0
  296. package/src/detectors/weak-hash.ts +217 -0
  297. package/src/detectors/webauthn.ts +205 -0
  298. package/src/detectors/xmldsig.ts +115 -0
  299. package/src/evidence.ts +138 -4
  300. package/src/index.ts +50 -12
  301. package/src/inventory.ts +15 -7
  302. package/src/policy.ts +180 -0
  303. package/src/registry.ts +81 -4
  304. package/src/remediate-pipeline.ts +26 -3
  305. package/src/remediation.ts +39 -0
  306. package/src/report.ts +43 -1
  307. package/src/scan-worker.ts +5 -3
  308. package/src/scan.ts +41 -6
  309. package/src/standards-profiles.ts +136 -0
  310. package/src/standards.ts +186 -0
  311. package/src/types.ts +11 -4
  312. package/src/version.ts +1 -1
  313. package/src/vex.ts +122 -0
  314. package/src/walk.ts +33 -2
@@ -1,4 +1,4 @@
1
- import { JS_TS_EXTENSIONS, DOC_EXTENSIONS, JWT_HOST_EXTENSIONS, eachMatch, findingFromRule, hasExtension, nearSortedCall, } from "../detect-utils.js";
1
+ import { JS_TS_EXTENSIONS, DOC_EXTENSIONS, JWT_HOST_EXTENSIONS, eachMatch, enclosingObject, 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.
@@ -46,10 +147,14 @@ const RE_TLS_REJECT = /rejectUnauthorized\s*:\s*false/g;
46
147
  // Hardened cipher regex: bounded spans (no unbounded `[^'"`]*` straddling the
47
148
  // alternation), single-quote-style anchoring removed in favour of {0,256} bounds
48
149
  // so worst-case backtracking is linear in the bound, not the file (P0-6).
49
- // The `(?<![!-])` lookbehind skips OpenSSL EXCLUSION syntax — `!MD5` / `-RC4`
50
- // DISABLE those ciphers, so a hardened list like `...:!aNULL:!MD5:!RC4` must not
51
- // be flagged as weak (audit: crypto #7). A genuinely-enabled `RC4` still matches.
52
- const RE_TLS_WEAK_CIPHER = /ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![!-])(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
150
+ // Skip OpenSSL EXCLUSION syntax — `!MD5` / `:-RC4` / `!ECDHE-RSA-RC4-SHA` DISABLE
151
+ // those ciphers, so a hardened list must not be flagged as weak (audit: crypto #7). The
152
+ // single variable-length lookbehind walks back over the suite-name chars to the
153
+ // ELEMENT boundary (`:` / whitespace / comma / quote / start) and rejects the match
154
+ // when that element begins with `!` or `-` — covering a bare `!RC4`, a space-separated
155
+ // `HIGH !RC4`, and a full-suite `!ECDHE-RSA-RC4-SHA`. The intra-name hyphen in a
156
+ // genuinely-enabled `ECDHE-RSA-RC4-SHA` (no leading `!`/`-`) still matches.
157
+ const RE_TLS_WEAK_CIPHER = /ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![:'"`,\s]\s*[!-][\w-]{0,64})(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
53
158
  /* -------------------------------------------------------------------------- */
54
159
  /* Node.js `crypto` module */
55
160
  /* -------------------------------------------------------------------------- */
@@ -177,52 +282,41 @@ const nodeCryptoDetector = {
177
282
  const findings = [];
178
283
  // generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
179
284
  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: "medium", hndl: true, label: "X25519" },
210
- x448: { algo: "X448", cat: "key-exchange", sev: "medium", 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
- }));
285
+ pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
225
286
  });
287
+ // Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
288
+ // the CommonJS destructure-rename) so an aliased call still detects. Only the
289
+ // keygen / ECDH / DH constructors are resolved — their classification is
290
+ // unambiguous. The alias regexes run on the ORIGINAL content, so locations
291
+ // stay exact; it fires only when the alias was explicitly bound to a known
292
+ // crypto symbol, so precision is unaffected.
293
+ const aliases = collectCryptoAliases(content);
294
+ for (const [canonical, names] of aliases) {
295
+ for (const alias of names) {
296
+ const a = escapeRe(alias);
297
+ if (canonical === "generateKeyPairSync" || canonical === "generateKeyPair") {
298
+ const re = new RegExp(`\\b${a}\\s*\\(\\s*['"\`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"\`]`, "g");
299
+ eachMatch(re, content, (m) => pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length));
300
+ }
301
+ else if (canonical === "createECDH") {
302
+ eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_ECDH, {
303
+ file,
304
+ content,
305
+ index: m.index,
306
+ matchLength: m[0].length,
307
+ })));
308
+ }
309
+ else {
310
+ // createDiffieHellman / createDiffieHellmanGroup.
311
+ eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_DH, {
312
+ file,
313
+ content,
314
+ index: m.index,
315
+ matchLength: m[0].length,
316
+ })));
317
+ }
318
+ }
319
+ }
226
320
  // createSign / createVerify — RSA / ECDSA / DSA signatures.
227
321
  eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
228
322
  findings.push(findingFromRule(RULE_NODE_SIGN, {
@@ -343,7 +437,9 @@ const webCryptoDetector = {
343
437
  algorithm = name === "X448" ? "X448" : "X25519";
344
438
  category = "key-exchange";
345
439
  hndl = true;
346
- severity = "low"; // modern but classical
440
+ // `medium`, aligned with X25519/X448 key agreement in node/rust/go — HNDL-exposed
441
+ // key agreement scores the same regardless of which surface uses it.
442
+ severity = "medium";
347
443
  }
348
444
  else if (name === "ED25519" || name === "ED448") {
349
445
  algorithm = "EdDSA";
@@ -485,7 +581,21 @@ const libraryDetector = {
485
581
  add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
486
582
  add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
487
583
  add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
488
- add(RE_SECP256K1, RULE_SECP256K1); // secp.sign / getPublicKey / getSharedSecret
584
+ // secp256k1: classify per method getSharedSecret/ecdh are key AGREEMENT
585
+ // (ECDH, harvest-now-decrypt-later); sign/verify/getPublicKey/recover are
586
+ // ECDSA signatures (not HNDL). The old rule flagged everything as ECDSA.
587
+ eachMatch(RE_SECP256K1, content, (m) => {
588
+ const kex = m[1] === "getSharedSecret" || m[1] === "ecdh";
589
+ findings.push(findingFromRule(RULE_SECP256K1, { file, content, index: m.index, matchLength: m[0].length }, kex
590
+ ? {
591
+ title: "secp256k1 ECDH key agreement",
592
+ category: "key-exchange",
593
+ algorithm: "ECDH",
594
+ hndl: true,
595
+ message: `secp256k1 ECDH key agreement (.${m[1]}()) is classical and harvest-now-decrypt-later exposed.`,
596
+ }
597
+ : undefined));
598
+ });
489
599
  add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
490
600
  add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
491
601
  add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
@@ -555,8 +665,21 @@ const jwtDetector = {
555
665
  appliesTo: (f) => hasExtension(f, JWT_HOST_EXTENSIONS),
556
666
  detect({ file, content }) {
557
667
  const findings = [];
668
+ // Deferral guards against double-counting:
669
+ // - webCryptoDetector owns a quoted `RSA-OAEP`/`ECDH-ES` sitting next to a
670
+ // `subtle.*(` call (it matches the `RSA-OAEP`/`ECDH` name). Collect the subtle
671
+ // call offsets so the JOSE key-management loops can skip those. (In non-JS host
672
+ // files there are no subtle calls, so nothing is skipped.)
673
+ // - jwkDetector owns any alg/`kty` that lives inside a JWK object; skip a token
674
+ // whose enclosing `{…}` carries a `"kty"` so a JWK's own `alg` is not counted by
675
+ // both detectors. Mirrors the same guard jose.ts applies for other languages.
676
+ const subtleCalls = [];
677
+ eachMatch(RE_SUBTLE_CALL, content, (m) => subtleCalls.push(m.index));
678
+ const inJwk = (index) => enclosingObject(content, index).includes('"kty"');
558
679
  // Classical JWS signature alg tokens. Anchored to quotes to avoid words.
559
680
  eachMatch(RE_JWT_ALG, content, (m) => {
681
+ if (inJwk(m.index))
682
+ return; // jwk-{rsa,ec,eddsa} owns a JWK's own alg
560
683
  const alg = m[1];
561
684
  let algorithm;
562
685
  if (alg.startsWith("RS") || alg.startsWith("PS"))
@@ -573,6 +696,10 @@ const jwtDetector = {
573
696
  });
574
697
  // JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
575
698
  eachMatch(RE_JOSE_ECDH, content, (m) => {
699
+ if (inJwk(m.index))
700
+ return; // jwk-ec owns a JWK's own alg
701
+ if (nearSortedCall(subtleCalls, m.index, 400))
702
+ return; // webcrypto owns it
576
703
  findings.push(findingFromRule(RULE_JOSE_ECDH, { file, content, index: m.index, matchLength: m[0].length }, {
577
704
  title: `JOSE key agreement ${m[1]}`,
578
705
  message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
@@ -580,6 +707,12 @@ const jwtDetector = {
580
707
  });
581
708
  // JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
582
709
  eachMatch(RE_JOSE_KEM, content, (m) => {
710
+ if (inJwk(m.index))
711
+ return; // jwk-rsa owns a JWK's own alg
712
+ // webCryptoDetector's regex matches `RSA-OAEP` but NOT `RSA1_5`, so only defer the
713
+ // OAEP form near a subtle call — deferring RSA1_5 would drop it entirely.
714
+ if (m[1].startsWith("RSA-OAEP") && nearSortedCall(subtleCalls, m.index, 400))
715
+ return;
583
716
  findings.push(findingFromRule(RULE_JOSE_RSA_OAEP, { file, content, index: m.index, matchLength: m[0].length }, {
584
717
  title: `JOSE RSA key transport ${m[1]}`,
585
718
  message: `JOSE "${m[1]}" is classical RSA key transport — harvest-now-decrypt-later exposed.`,
@@ -678,6 +811,55 @@ const RE_CERT_SIG_ALG = /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA
678
811
  // Shor-broken key agreement — the harvest-now-decrypt-later surface a lexical
679
812
  // scan misses when only the `crypto/*` API is matched, not the negotiated kex.
680
813
  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;
814
+ /**
815
+ * True when the match at `index` sits inside a DOCUMENTATION field value — a
816
+ * `description` / `help` / `summary` / `doc` / `comment` / `note` assignment
817
+ * (`key = "…"` or `key: "…"`), as used by Terraform/Packer variable descriptions,
818
+ * JSON-schema docs, etc. Classical KEX/cipher algorithm names LISTED in such a field
819
+ * are prose ("acceptable values include ecdh-sha2-nistp256, …"), not an active
820
+ * setting, so a transport-token rule must not fire on them. Scans the current line's
821
+ * prefix only (cheap; the common single-line description case).
822
+ */
823
+ function inDocFieldValue(content, index) {
824
+ const lineStart = content.lastIndexOf("\n", index - 1) + 1;
825
+ const prefix = content.slice(lineStart, index);
826
+ return /\b(?:description|help|summary|doc|comment|note)\b\s*[:=]/i.test(prefix);
827
+ }
828
+ // A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
829
+ // when it is EITHER (a) followed by base64 key material — an actual
830
+ // authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
831
+ // algorithm tokens on the same line — an algorithm-preference list
832
+ // (`HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,ssh-rsa`, `%w[…]`, …). A
833
+ // lone token, e.g. a UI label / i18n value like `"ssh-rsa": "ssh-rsa"`, is
834
+ // neither, so it must NOT be flagged. `RE_SSH_ALGO_TOKEN` is a separate (cloned-
835
+ // on-use) copy so counting per line never disturbs the outer `eachMatch` state.
836
+ const RE_SSH_ALGO_TOKEN = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
837
+ /** How far around a token we look for a sibling algorithm on the same line. A
838
+ * real `HostKeyAlgorithms` list is short; bounding the window keeps the per-token
839
+ * work O(1) so a pathological single huge line can't make detection quadratic. */
840
+ const SSH_LINE_WINDOW = 512;
841
+ function isRealSshKeyOrAlgoList(content, index, matchLen) {
842
+ // (a) base64 key material immediately after the token → a real SSH public key.
843
+ if (/^\s+[A-Za-z0-9+/]{20,}/.test(content.slice(index + matchLen, index + matchLen + 80))) {
844
+ return true;
845
+ }
846
+ // (b) ≥2 distinct ssh key/host-key algorithm tokens on the same line → a list.
847
+ // Bound both the back-scan for the line start and the window itself so the work
848
+ // is constant per token and never crosses a line boundary (i18n entries live on
849
+ // separate lines, so a lone repeated token stays a single distinct token).
850
+ const from = Math.max(0, index - SSH_LINE_WINDOW);
851
+ const pre = content.slice(from, index);
852
+ const preNl = pre.lastIndexOf("\n");
853
+ const lineStart = preNl === -1 ? from : from + preNl + 1;
854
+ const to = Math.min(content.length, index + matchLen + SSH_LINE_WINDOW);
855
+ const post = content.slice(index, to);
856
+ const postNl = post.indexOf("\n");
857
+ const lineEnd = postNl === -1 ? to : index + postNl;
858
+ const distinct = new Set();
859
+ for (const t of content.slice(lineStart, lineEnd).matchAll(RE_SSH_ALGO_TOKEN))
860
+ distinct.add(t[1]);
861
+ return distinct.size >= 2;
862
+ }
681
863
  const RULE_SSH_PUBKEY = {
682
864
  id: "ssh-public-key",
683
865
  title: "Classical SSH public key",
@@ -737,6 +919,10 @@ const sshCertDetector = {
737
919
  const findings = [];
738
920
  // SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
739
921
  eachMatch(RE_SSH_PUBKEY, content, (m) => {
922
+ // Skip a bare token that is neither a real key nor an algorithm list (e.g.
923
+ // a `"ssh-rsa": "ssh-rsa"` i18n label) — see `isRealSshKeyOrAlgoList`.
924
+ if (!isRealSshKeyOrAlgoList(content, m.index, m[0].length))
925
+ return;
740
926
  const tok = m[1];
741
927
  const algorithm = tok.startsWith("ssh-rsa")
742
928
  ? "RSA"
@@ -767,6 +953,8 @@ const sshCertDetector = {
767
953
  });
768
954
  // SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
769
955
  eachMatch(RE_SSH_KEX, content, (m) => {
956
+ if (inDocFieldValue(content, m.index))
957
+ return; // names listed in a doc string
770
958
  const tok = m[1];
771
959
  const algorithm = tok.startsWith("diffie-hellman")
772
960
  ? "DH"
@@ -822,6 +1010,8 @@ const tlsClassicalKexDetector = {
822
1010
  detect({ file, content }) {
823
1011
  const findings = [];
824
1012
  eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
1013
+ if (inDocFieldValue(content, m.index))
1014
+ return; // names listed in a doc string
825
1015
  const tok = m[0];
826
1016
  const algorithm = tok.includes("ECDH") ? "ECDH" : "DH";
827
1017
  findings.push(findingFromRule(RULE_TLS_CLASSICAL_KEX, { file, content, index: m.index, matchLength: m[0].length }, {