@quantakrypto/core 0.4.4 → 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 (284) hide show
  1. package/README.md +76 -27
  2. package/dist/cbom-merge.d.ts.map +1 -1
  3. package/dist/cbom-merge.js +30 -8
  4. package/dist/cbom-merge.js.map +1 -1
  5. package/dist/cbom.d.ts +5 -0
  6. package/dist/cbom.d.ts.map +1 -1
  7. package/dist/cbom.js +132 -37
  8. package/dist/cbom.js.map +1 -1
  9. package/dist/comments.d.ts +2 -23
  10. package/dist/comments.d.ts.map +1 -1
  11. package/dist/comments.js +24 -5
  12. package/dist/comments.js.map +1 -1
  13. package/dist/detect-utils.d.ts +55 -21
  14. package/dist/detect-utils.d.ts.map +1 -1
  15. package/dist/detect-utils.js +133 -7
  16. package/dist/detect-utils.js.map +1 -1
  17. package/dist/detectors/age.d.ts +16 -0
  18. package/dist/detectors/age.d.ts.map +1 -0
  19. package/dist/detectors/age.js +40 -0
  20. package/dist/detectors/age.js.map +1 -0
  21. package/dist/detectors/ansible.d.ts +13 -0
  22. package/dist/detectors/ansible.d.ts.map +1 -0
  23. package/dist/detectors/ansible.js +108 -0
  24. package/dist/detectors/ansible.js.map +1 -0
  25. package/dist/detectors/bicep.d.ts +20 -0
  26. package/dist/detectors/bicep.d.ts.map +1 -0
  27. package/dist/detectors/bicep.js +74 -0
  28. package/dist/detectors/bicep.js.map +1 -0
  29. package/dist/detectors/c.d.ts.map +1 -1
  30. package/dist/detectors/c.js +30 -3
  31. package/dist/detectors/c.js.map +1 -1
  32. package/dist/detectors/cicd.d.ts.map +1 -1
  33. package/dist/detectors/cicd.js +18 -5
  34. package/dist/detectors/cicd.js.map +1 -1
  35. package/dist/detectors/cloud-kms.d.ts +8 -5
  36. package/dist/detectors/cloud-kms.d.ts.map +1 -1
  37. package/dist/detectors/cloud-kms.js +62 -20
  38. package/dist/detectors/cloud-kms.js.map +1 -1
  39. package/dist/detectors/cloudformation.d.ts +8 -0
  40. package/dist/detectors/cloudformation.d.ts.map +1 -1
  41. package/dist/detectors/cloudformation.js +41 -10
  42. package/dist/detectors/cloudformation.js.map +1 -1
  43. package/dist/detectors/codesign.d.ts +50 -0
  44. package/dist/detectors/codesign.d.ts.map +1 -0
  45. package/dist/detectors/codesign.js +143 -0
  46. package/dist/detectors/codesign.js.map +1 -0
  47. package/dist/detectors/csharp.d.ts.map +1 -1
  48. package/dist/detectors/csharp.js +13 -3
  49. package/dist/detectors/csharp.js.map +1 -1
  50. package/dist/detectors/dart.d.ts +35 -0
  51. package/dist/detectors/dart.d.ts.map +1 -0
  52. package/dist/detectors/dart.js +177 -0
  53. package/dist/detectors/dart.js.map +1 -0
  54. package/dist/detectors/database.d.ts +5 -3
  55. package/dist/detectors/database.d.ts.map +1 -1
  56. package/dist/detectors/database.js +45 -8
  57. package/dist/detectors/database.js.map +1 -1
  58. package/dist/detectors/dkim.d.ts +54 -0
  59. package/dist/detectors/dkim.d.ts.map +1 -0
  60. package/dist/detectors/dkim.js +92 -0
  61. package/dist/detectors/dkim.js.map +1 -0
  62. package/dist/detectors/dnssec.d.ts.map +1 -1
  63. package/dist/detectors/dnssec.js +19 -10
  64. package/dist/detectors/dnssec.js.map +1 -1
  65. package/dist/detectors/elixir.d.ts.map +1 -1
  66. package/dist/detectors/elixir.js +34 -2
  67. package/dist/detectors/elixir.js.map +1 -1
  68. package/dist/detectors/java.d.ts.map +1 -1
  69. package/dist/detectors/java.js +33 -3
  70. package/dist/detectors/java.js.map +1 -1
  71. package/dist/detectors/jose.d.ts.map +1 -1
  72. package/dist/detectors/jose.js +13 -3
  73. package/dist/detectors/jose.js.map +1 -1
  74. package/dist/detectors/jwk.d.ts +7 -7
  75. package/dist/detectors/jwk.d.ts.map +1 -1
  76. package/dist/detectors/jwk.js +44 -8
  77. package/dist/detectors/jwk.js.map +1 -1
  78. package/dist/detectors/k8s.d.ts.map +1 -1
  79. package/dist/detectors/k8s.js +5 -2
  80. package/dist/detectors/k8s.js.map +1 -1
  81. package/dist/detectors/keystore.d.ts +19 -0
  82. package/dist/detectors/keystore.d.ts.map +1 -0
  83. package/dist/detectors/keystore.js +76 -0
  84. package/dist/detectors/keystore.js.map +1 -0
  85. package/dist/detectors/mesh.d.ts +7 -8
  86. package/dist/detectors/mesh.d.ts.map +1 -1
  87. package/dist/detectors/mesh.js +17 -35
  88. package/dist/detectors/mesh.js.map +1 -1
  89. package/dist/detectors/messaging.d.ts +2 -2
  90. package/dist/detectors/messaging.d.ts.map +1 -1
  91. package/dist/detectors/messaging.js +19 -12
  92. package/dist/detectors/messaging.js.map +1 -1
  93. package/dist/detectors/objc.d.ts +41 -0
  94. package/dist/detectors/objc.d.ts.map +1 -0
  95. package/dist/detectors/objc.js +146 -0
  96. package/dist/detectors/objc.js.map +1 -0
  97. package/dist/detectors/openpgp.d.ts +20 -0
  98. package/dist/detectors/openpgp.d.ts.map +1 -0
  99. package/dist/detectors/openpgp.js +151 -0
  100. package/dist/detectors/openpgp.js.map +1 -0
  101. package/dist/detectors/pem.d.ts.map +1 -1
  102. package/dist/detectors/pem.js +32 -0
  103. package/dist/detectors/pem.js.map +1 -1
  104. package/dist/detectors/php.d.ts +2 -1
  105. package/dist/detectors/php.d.ts.map +1 -1
  106. package/dist/detectors/php.js +7 -3
  107. package/dist/detectors/php.js.map +1 -1
  108. package/dist/detectors/pkcs11.d.ts +17 -0
  109. package/dist/detectors/pkcs11.d.ts.map +1 -0
  110. package/dist/detectors/pkcs11.js +87 -0
  111. package/dist/detectors/pkcs11.js.map +1 -0
  112. package/dist/detectors/proxy.d.ts +52 -0
  113. package/dist/detectors/proxy.d.ts.map +1 -0
  114. package/dist/detectors/proxy.js +157 -0
  115. package/dist/detectors/proxy.js.map +1 -0
  116. package/dist/detectors/pulumi.d.ts +19 -0
  117. package/dist/detectors/pulumi.d.ts.map +1 -0
  118. package/dist/detectors/pulumi.js +80 -0
  119. package/dist/detectors/pulumi.js.map +1 -0
  120. package/dist/detectors/rust.d.ts.map +1 -1
  121. package/dist/detectors/rust.js +5 -0
  122. package/dist/detectors/rust.js.map +1 -1
  123. package/dist/detectors/secrets.d.ts +4 -1
  124. package/dist/detectors/secrets.d.ts.map +1 -1
  125. package/dist/detectors/secrets.js +5 -21
  126. package/dist/detectors/secrets.js.map +1 -1
  127. package/dist/detectors/solidity.d.ts +50 -0
  128. package/dist/detectors/solidity.d.ts.map +1 -0
  129. package/dist/detectors/solidity.js +108 -0
  130. package/dist/detectors/solidity.js.map +1 -0
  131. package/dist/detectors/source.d.ts.map +1 -1
  132. package/dist/detectors/source.js +53 -6
  133. package/dist/detectors/source.js.map +1 -1
  134. package/dist/detectors/spire.d.ts +49 -0
  135. package/dist/detectors/spire.d.ts.map +1 -0
  136. package/dist/detectors/spire.js +77 -0
  137. package/dist/detectors/spire.js.map +1 -0
  138. package/dist/detectors/ssh-ca.d.ts +72 -0
  139. package/dist/detectors/ssh-ca.d.ts.map +1 -0
  140. package/dist/detectors/ssh-ca.js +119 -0
  141. package/dist/detectors/ssh-ca.js.map +1 -0
  142. package/dist/detectors/supply-chain.d.ts +17 -0
  143. package/dist/detectors/supply-chain.d.ts.map +1 -0
  144. package/dist/detectors/supply-chain.js +89 -0
  145. package/dist/detectors/supply-chain.js.map +1 -0
  146. package/dist/detectors/swift.d.ts +22 -0
  147. package/dist/detectors/swift.d.ts.map +1 -0
  148. package/dist/detectors/swift.js +120 -0
  149. package/dist/detectors/swift.js.map +1 -0
  150. package/dist/detectors/terraform.d.ts.map +1 -1
  151. package/dist/detectors/terraform.js +35 -8
  152. package/dist/detectors/terraform.js.map +1 -1
  153. package/dist/detectors/vault.d.ts +15 -0
  154. package/dist/detectors/vault.d.ts.map +1 -0
  155. package/dist/detectors/vault.js +97 -0
  156. package/dist/detectors/vault.js.map +1 -0
  157. package/dist/detectors/vpn.d.ts +8 -8
  158. package/dist/detectors/vpn.d.ts.map +1 -1
  159. package/dist/detectors/vpn.js +12 -38
  160. package/dist/detectors/vpn.js.map +1 -1
  161. package/dist/detectors/weak-hash.d.ts +71 -0
  162. package/dist/detectors/weak-hash.d.ts.map +1 -0
  163. package/dist/detectors/weak-hash.js +116 -0
  164. package/dist/detectors/weak-hash.js.map +1 -0
  165. package/dist/detectors/webauthn.d.ts +61 -0
  166. package/dist/detectors/webauthn.d.ts.map +1 -0
  167. package/dist/detectors/webauthn.js +109 -0
  168. package/dist/detectors/webauthn.js.map +1 -0
  169. package/dist/detectors/xmldsig.d.ts +23 -0
  170. package/dist/detectors/xmldsig.d.ts.map +1 -0
  171. package/dist/detectors/xmldsig.js +84 -0
  172. package/dist/detectors/xmldsig.js.map +1 -0
  173. package/dist/evidence.d.ts +74 -4
  174. package/dist/evidence.d.ts.map +1 -1
  175. package/dist/evidence.js +71 -0
  176. package/dist/evidence.js.map +1 -1
  177. package/dist/index.d.ts +14 -11
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +17 -11
  180. package/dist/index.js.map +1 -1
  181. package/dist/inventory.d.ts +0 -2
  182. package/dist/inventory.d.ts.map +1 -1
  183. package/dist/inventory.js +15 -6
  184. package/dist/inventory.js.map +1 -1
  185. package/dist/registry.d.ts +9 -4
  186. package/dist/registry.d.ts.map +1 -1
  187. package/dist/registry.js +51 -4
  188. package/dist/registry.js.map +1 -1
  189. package/dist/remediate-pipeline.d.ts +0 -3
  190. package/dist/remediate-pipeline.d.ts.map +1 -1
  191. package/dist/remediate-pipeline.js +1 -1
  192. package/dist/remediate-pipeline.js.map +1 -1
  193. package/dist/remediation.d.ts +10 -0
  194. package/dist/remediation.d.ts.map +1 -1
  195. package/dist/remediation.js +32 -0
  196. package/dist/remediation.js.map +1 -1
  197. package/dist/report.d.ts +9 -0
  198. package/dist/report.d.ts.map +1 -1
  199. package/dist/report.js +31 -1
  200. package/dist/report.js.map +1 -1
  201. package/dist/scan-worker.js +5 -3
  202. package/dist/scan-worker.js.map +1 -1
  203. package/dist/scan.d.ts.map +1 -1
  204. package/dist/scan.js +35 -6
  205. package/dist/scan.js.map +1 -1
  206. package/dist/standards-profiles.d.ts +58 -0
  207. package/dist/standards-profiles.d.ts.map +1 -0
  208. package/dist/standards-profiles.js +100 -0
  209. package/dist/standards-profiles.js.map +1 -0
  210. package/dist/types.d.ts +6 -5
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/types.js.map +1 -1
  213. package/dist/version.d.ts +1 -1
  214. package/dist/version.js +1 -1
  215. package/dist/version.js.map +1 -1
  216. package/dist/vex.d.ts +39 -0
  217. package/dist/vex.d.ts.map +1 -0
  218. package/dist/vex.js +87 -0
  219. package/dist/vex.js.map +1 -0
  220. package/dist/walk.d.ts +8 -3
  221. package/dist/walk.d.ts.map +1 -1
  222. package/dist/walk.js +32 -1
  223. package/dist/walk.js.map +1 -1
  224. package/package.json +1 -1
  225. package/src/cbom-merge.ts +38 -8
  226. package/src/cbom.ts +153 -39
  227. package/src/comments.ts +25 -6
  228. package/src/detect-utils.ts +133 -11
  229. package/src/detectors/age.ts +60 -0
  230. package/src/detectors/ansible.ts +140 -0
  231. package/src/detectors/bicep.ts +107 -0
  232. package/src/detectors/c.ts +31 -3
  233. package/src/detectors/cicd.ts +18 -5
  234. package/src/detectors/cloud-kms.ts +79 -25
  235. package/src/detectors/cloudformation.ts +45 -10
  236. package/src/detectors/codesign.ts +214 -0
  237. package/src/detectors/csharp.ts +13 -3
  238. package/src/detectors/dart.ts +237 -0
  239. package/src/detectors/database.ts +64 -11
  240. package/src/detectors/dkim.ts +172 -0
  241. package/src/detectors/dnssec.ts +19 -10
  242. package/src/detectors/elixir.ts +33 -2
  243. package/src/detectors/java.ts +37 -3
  244. package/src/detectors/jose.ts +19 -3
  245. package/src/detectors/jwk.ts +59 -17
  246. package/src/detectors/k8s.ts +5 -2
  247. package/src/detectors/keystore.ts +109 -0
  248. package/src/detectors/mesh.ts +21 -45
  249. package/src/detectors/messaging.ts +22 -14
  250. package/src/detectors/objc.ts +209 -0
  251. package/src/detectors/openpgp.ts +194 -0
  252. package/src/detectors/pem.ts +30 -0
  253. package/src/detectors/php.ts +9 -4
  254. package/src/detectors/pkcs11.ts +113 -0
  255. package/src/detectors/proxy.ts +236 -0
  256. package/src/detectors/pulumi.ts +106 -0
  257. package/src/detectors/rust.ts +5 -0
  258. package/src/detectors/secrets.ts +15 -26
  259. package/src/detectors/solidity.ts +176 -0
  260. package/src/detectors/source.ts +48 -5
  261. package/src/detectors/spire.ts +143 -0
  262. package/src/detectors/ssh-ca.ts +215 -0
  263. package/src/detectors/supply-chain.ts +117 -0
  264. package/src/detectors/swift.ts +149 -0
  265. package/src/detectors/terraform.ts +42 -8
  266. package/src/detectors/vault.ts +120 -0
  267. package/src/detectors/vpn.ts +20 -48
  268. package/src/detectors/weak-hash.ts +217 -0
  269. package/src/detectors/webauthn.ts +205 -0
  270. package/src/detectors/xmldsig.ts +115 -0
  271. package/src/evidence.ts +127 -4
  272. package/src/index.ts +41 -13
  273. package/src/inventory.ts +15 -7
  274. package/src/registry.ts +51 -4
  275. package/src/remediate-pipeline.ts +1 -1
  276. package/src/remediation.ts +39 -0
  277. package/src/report.ts +36 -1
  278. package/src/scan-worker.ts +5 -3
  279. package/src/scan.ts +41 -6
  280. package/src/standards-profiles.ts +136 -0
  281. package/src/types.ts +9 -4
  282. package/src/version.ts +1 -1
  283. package/src/vex.ts +122 -0
  284. package/src/walk.ts +33 -2
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Source detector for classical asymmetric cryptography in Dart / Flutter.
3
+ *
4
+ * Dart has no asymmetric crypto in its standard library, so real-world code
5
+ * reaches for one of the two dominant pub.dev packages, and both expose their
6
+ * primitives through distinctive class names — which makes a lexical scan (the
7
+ * same zero-dependency strategy as every other language pack here) precise:
8
+ *
9
+ * - package:pointycastle (a Bouncy Castle port). RSA via `RSAKeyGenerator`,
10
+ * `RSAEngine`, `RSASigner`, `PSSSigner`; EC via `ECKeyGenerator`,
11
+ * `ECDSASigner`, `ECDHBasicAgreement`; Edwards via `Ed25519`.
12
+ * - package:cryptography (Dart-native). RSA via `RsaPss`, `RsaSsaPkcs1v15`;
13
+ * EC via `Ecdsa`, `Ecdh`; modern curves via `Ed25519`, `X25519`.
14
+ *
15
+ * HNDL (harvest-now-decrypt-later) reasoning — the field that drives urgency:
16
+ * - RSA encryption (`RSAEngine`) and RSA key generation (`RSAKeyGenerator`,
17
+ * which yields keys that can decrypt) are confidentiality surfaces → hndl:true.
18
+ * - (EC)DH key agreement (`ECDHBasicAgreement`, `Ecdh`, `X25519`) is the classic
19
+ * HNDL target — recorded ciphertext is decryptable once the curve falls → hndl:true.
20
+ * - Signatures (`RSASigner`/`PSSSigner`/`RsaPss`/`RsaSsaPkcs1v15`, `ECDSASigner`/
21
+ * `Ecdsa`, `Ed25519`) are forgeable by a quantum attacker but not retroactively
22
+ * breakable → hndl:false.
23
+ * - `ECKeyGenerator` is ambiguous (an EC key can feed either ECDSA or ECDH); it is
24
+ * classified conservatively as an ECDSA signing key (hndl:false), the Dart default.
25
+ *
26
+ * Comment suppression is C-style (`//` line + `/* … *\/` block), matching Dart's
27
+ * lexer, so commented-out crypto is not reported. The regexes are `\b`-anchored on
28
+ * the library-specific identifiers above; none matches a bare `RSA`/`EC` substring,
29
+ * keeping false positives low. A cheap substring fast-reject skips the regex work on
30
+ * the overwhelming majority of `.dart` files that touch no asymmetric crypto.
31
+ */
32
+ import type { Detector, Finding, RuleMeta } from "../types.js";
33
+ import {
34
+ eachMatch,
35
+ findingFromRule,
36
+ hasExtension,
37
+ maskBlockComments,
38
+ maskCommentLines,
39
+ DART_EXTENSIONS,
40
+ } from "../detect-utils.js";
41
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
42
+
43
+ // RSA key generation / raw RSA encryption engine (pointycastle). Both are
44
+ // confidentiality-capable (a keypair can decrypt; the engine encrypts) → kem.
45
+ const RE_DART_RSA_KEYGEN = /\b(?:RSAKeyGenerator|RSAEngine)\b/g;
46
+ // RSA signature schemes: pointycastle `RSASigner`/`PSSSigner`, cryptography
47
+ // `RsaPss`/`RsaSsaPkcs1v15`. Distinct casing keeps these off the keygen rule.
48
+ const RE_DART_RSA_SIGN = /\b(?:RSASigner|PSSSigner|RsaPss|RsaSsaPkcs1v15)\b/g;
49
+ // ECDSA SIGNING: pointycastle `ECDSASigner`, cryptography `Ecdsa`. `\bEcdsa\b`
50
+ // requires a boundary, so `EcdsaP256`-style concatenations (rare) and the all-caps
51
+ // `ECDSASigner` don't double-fire here. (EC keygen is a SEPARATE, HNDL-exposed rule.)
52
+ const RE_DART_ECDSA = /\b(?:ECDSASigner|Ecdsa)\b/g;
53
+ // EC KEY GENERATION (pointycastle `ECKeyGenerator`) is ambiguous — an EC key can
54
+ // feed ECDSA signing OR ECDH agreement — so it is classified key-exchange/ECDH/
55
+ // hndl:true (the fleet's HNDL-safe convention), NOT folded into the signing rule.
56
+ const RE_DART_EC_KEYGEN = /\bECKeyGenerator\b/g;
57
+ // ECDH key agreement: pointycastle `ECDHBasicAgreement`, cryptography `Ecdh`.
58
+ // `\bEcdh\b` cannot match inside `Ecdsa` (different letters), so no overlap.
59
+ const RE_DART_ECDH = /\b(?:ECDHBasicAgreement|Ecdh)\b/g;
60
+ // Ed25519 signer (both packages spell it `Ed25519`).
61
+ const RE_DART_ED25519 = /\bEd25519\b/g;
62
+ // X25519 key agreement (cryptography `X25519`).
63
+ const RE_DART_X25519 = /\bX25519\b/g;
64
+
65
+ // Cheap fast-reject: distinctive substrings that gate the (precise) regex work.
66
+ // If none is present the file cannot contain any of the tracked identifiers.
67
+ const DART_FAST_REJECT: readonly string[] = [
68
+ "RSA",
69
+ "Rsa",
70
+ "ECDSA",
71
+ "Ecdsa",
72
+ "ECDH",
73
+ "Ecdh",
74
+ "ECKeyGenerator",
75
+ "PSSSigner",
76
+ "Ed25519",
77
+ "X25519",
78
+ ];
79
+
80
+ const RULE_DART_RSA_KEYGEN: RuleMeta = {
81
+ id: "dart-rsa-keygen",
82
+ title: "Dart RSA key generation / encryption",
83
+ description: "pointycastle RSAKeyGenerator / RSAEngine",
84
+ category: "kem",
85
+ severity: "high",
86
+ confidence: "high",
87
+ algorithm: "RSA",
88
+ hndl: true,
89
+ cwe: CWE_BROKEN_CRYPTO,
90
+ message:
91
+ "Classical RSA key generation / encryption (Dart, pointycastle) is not quantum-safe and RSA encryption is harvest-now-decrypt-later exposed.",
92
+ remediation:
93
+ "Migrate to PQC (ML-KEM-768 for encryption / key transport) as Dart crypto packages add support.",
94
+ };
95
+ const RULE_DART_RSA_SIGN: RuleMeta = {
96
+ id: "dart-rsa-sign",
97
+ title: "Dart RSA signature",
98
+ description: "pointycastle RSASigner/PSSSigner, cryptography RsaPss/RsaSsaPkcs1v15",
99
+ category: "signature",
100
+ // `high`, consistent with every sibling pack's signature severity (go/java/php/
101
+ // objc/elixir RSA-sign) — the same primitive must not flip CI exit codes by language.
102
+ severity: "high",
103
+ confidence: "high",
104
+ algorithm: "RSA",
105
+ hndl: false,
106
+ cwe: CWE_BROKEN_CRYPTO,
107
+ message: "Classical RSA signature (Dart) is forgeable by a quantum attacker.",
108
+ remediation:
109
+ "Migrate to PQC signatures (ML-DSA-65, FIPS 204) as Dart crypto packages add support.",
110
+ };
111
+ const RULE_DART_ECDSA: RuleMeta = {
112
+ id: "dart-ecdsa",
113
+ title: "Dart ECDSA signature",
114
+ description: "pointycastle ECDSASigner, cryptography Ecdsa",
115
+ category: "signature",
116
+ severity: "high",
117
+ confidence: "high",
118
+ algorithm: "ECDSA",
119
+ hndl: false,
120
+ cwe: CWE_BROKEN_CRYPTO,
121
+ message: "Classical ECDSA signing (Dart) is forgeable by a quantum attacker.",
122
+ remediation:
123
+ "Migrate to PQC signatures (ML-DSA-65, FIPS 204) as Dart crypto packages add support.",
124
+ };
125
+ const RULE_DART_EC_KEYGEN: RuleMeta = {
126
+ id: "dart-ec-keygen",
127
+ title: "Dart EC key generation",
128
+ description: "pointycastle ECKeyGenerator (ambiguous EC key: ECDSA or ECDH)",
129
+ // Ambiguous EC keygen → key-exchange/ECDH/hndl:true (fleet HNDL-safe convention),
130
+ // since the key may feed ECDH agreement (harvest-now-decrypt-later exposed).
131
+ category: "key-exchange",
132
+ severity: "high",
133
+ confidence: "high",
134
+ algorithm: "ECDH",
135
+ hndl: true,
136
+ cwe: CWE_BROKEN_CRYPTO,
137
+ message:
138
+ "EC key generation (Dart, pointycastle); an EC key can feed ECDH key agreement (harvest-now-decrypt-later exposed) as well as ECDSA signing, and is not quantum-safe.",
139
+ remediation:
140
+ "For key agreement migrate to hybrid X25519MLKEM768 (ML-KEM-768); for signatures ML-DSA-65 — as Dart crypto packages add support.",
141
+ };
142
+ const RULE_DART_ECDH: RuleMeta = {
143
+ id: "dart-ecdh",
144
+ title: "Dart ECDH key agreement",
145
+ description: "pointycastle ECDHBasicAgreement, cryptography Ecdh",
146
+ category: "key-exchange",
147
+ severity: "high",
148
+ confidence: "high",
149
+ algorithm: "ECDH",
150
+ hndl: true,
151
+ cwe: CWE_BROKEN_CRYPTO,
152
+ message:
153
+ "Elliptic-curve Diffie-Hellman key agreement (Dart) is broken by Shor's algorithm (harvest-now-decrypt-later).",
154
+ remediation:
155
+ "Migrate key agreement to hybrid X25519MLKEM768 (ML-KEM-768) as Dart crypto packages add support.",
156
+ };
157
+ const RULE_DART_ED25519: RuleMeta = {
158
+ id: "dart-ed25519",
159
+ title: "Dart Ed25519 signature",
160
+ description: "pointycastle / cryptography Ed25519 signer",
161
+ category: "signature",
162
+ // `low`, aligned with Ed25519 across the other source packs (swift/rust/go/…) —
163
+ // the same primitive must not flip CI exit codes based on which language wrote it.
164
+ severity: "low",
165
+ confidence: "high",
166
+ algorithm: "EdDSA",
167
+ hndl: false,
168
+ cwe: CWE_BROKEN_CRYPTO,
169
+ message:
170
+ "Ed25519 (Dart) is a modern but still classical signature scheme, forgeable by a quantum attacker.",
171
+ remediation:
172
+ "Migrate to PQC signatures (ML-DSA-65, FIPS 204) as Dart crypto packages add support.",
173
+ };
174
+ const RULE_DART_X25519: RuleMeta = {
175
+ id: "dart-x25519",
176
+ title: "Dart X25519 key agreement",
177
+ description: "cryptography X25519 key agreement",
178
+ category: "key-exchange",
179
+ severity: "medium",
180
+ confidence: "high",
181
+ algorithm: "X25519",
182
+ hndl: true,
183
+ cwe: CWE_BROKEN_CRYPTO,
184
+ message:
185
+ "X25519 (Dart) is modern but still classical key agreement — harvest-now-decrypt-later exposed.",
186
+ remediation:
187
+ "Migrate key agreement to hybrid X25519MLKEM768 (ML-KEM-768) as Dart crypto packages add support.",
188
+ };
189
+
190
+ /** Detects classical asymmetric crypto in Dart / Flutter (pointycastle, cryptography). */
191
+ export const dartDetector: Detector = {
192
+ id: "dart-crypto",
193
+ description: "Classical asymmetric crypto in Dart / Flutter (pointycastle, cryptography)",
194
+ scope: "source",
195
+ language: "dart",
196
+ rules: [
197
+ RULE_DART_RSA_KEYGEN,
198
+ RULE_DART_RSA_SIGN,
199
+ RULE_DART_ECDSA,
200
+ RULE_DART_EC_KEYGEN,
201
+ RULE_DART_ECDH,
202
+ RULE_DART_ED25519,
203
+ RULE_DART_X25519,
204
+ ],
205
+ appliesTo: (f) => hasExtension(f, DART_EXTENSIONS),
206
+ detect({ file, content }): Finding[] {
207
+ // Fast-reject: nothing to do unless a distinctive token appears.
208
+ if (!DART_FAST_REJECT.some((t) => content.includes(t))) return [];
209
+
210
+ // Suppress C-style comments so commented-out crypto isn't reported. Offsets
211
+ // are preserved (chars → spaces), so finding line/column stay exact.
212
+ const masked = maskCommentLines(maskBlockComments(content), ["//"]);
213
+
214
+ const findings: Finding[] = [];
215
+ // Scan the masked text, but build the finding (its snippet) from the ORIGINAL
216
+ // `content` so a line with a trailing comment renders live, not blanked.
217
+ const add = (re: RegExp, rule: RuleMeta) =>
218
+ eachMatch(re, masked, (m) =>
219
+ findings.push(
220
+ findingFromRule(rule, {
221
+ file,
222
+ content,
223
+ index: m.index,
224
+ matchLength: m[0].length,
225
+ }),
226
+ ),
227
+ );
228
+ add(RE_DART_RSA_KEYGEN, RULE_DART_RSA_KEYGEN);
229
+ add(RE_DART_RSA_SIGN, RULE_DART_RSA_SIGN);
230
+ add(RE_DART_ECDSA, RULE_DART_ECDSA);
231
+ add(RE_DART_EC_KEYGEN, RULE_DART_EC_KEYGEN);
232
+ add(RE_DART_ECDH, RULE_DART_ECDH);
233
+ add(RE_DART_ED25519, RULE_DART_ED25519);
234
+ add(RE_DART_X25519, RULE_DART_X25519);
235
+ return findings;
236
+ },
237
+ };
@@ -7,16 +7,34 @@
7
7
  * Covered:
8
8
  * - pgcrypto `pgp_pub_encrypt` / `pgp_pub_decrypt` in `.sql` (RSA/ElGamal public-key
9
9
  * encryption of column data) → `hndl:true`.
10
- * - libpq `sslmode = disable | allow | prefer | require` in any config/connection
11
- * file — a mode that does NOT verify the server certificate, so the classical
12
- * TLS session it does (or doesn't) negotiate is both MITM-able and harvestable.
10
+ * - libpq `sslmode = allow | prefer | require` in any config/connection file — a
11
+ * mode that does NOT verify the server certificate, so the classical TLS session
12
+ * it negotiates is both MITM-able and harvestable. `disable` is excluded: it
13
+ * negotiates no TLS at all, so there is no key exchange to harvest (a plaintext
14
+ * concern, out of scope for a PQC-readiness scanner).
13
15
  */
14
16
  import type { Detector, Finding, RuleMeta } from "../types.js";
15
- import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
17
+ import {
18
+ DOC_EXTENSIONS,
19
+ eachMatch,
20
+ findingFromRule,
21
+ hasExtension,
22
+ maskCommentLines,
23
+ } from "../detect-utils.js";
16
24
  import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION } from "../cwe.js";
17
25
 
18
26
  const RE_PGCRYPTO = /\bpgp_pub_(?:encrypt|decrypt)\b/g;
19
- const RE_WEAK_SSLMODE = /\bsslmode\s*=\s*["']?(?:disable|allow|prefer|require)\b/gi;
27
+ // Matches the no-verification TLS modes across libpq (`sslmode=require`), the
28
+ // `PGSSLMODE` env var, MySQL (`ssl-mode=PREFERRED`/`REQUIRED`, hyphenated), and YAML
29
+ // connection config (`sslmode: require`) — i.e. an optional `pg` prefix, `-`/`_` or no
30
+ // separator, `:` or `=`, and the libpq/MySQL value spellings.
31
+ const RE_WEAK_SSLMODE =
32
+ /\b(?:pg)?ssl[-_]?mode\s*[:=]\s*["']?(?:allow|prefer(?:red)?|require[d]?)\b/gi;
33
+ // SQL Server TDE: the database encryption key is protected by a classical RSA
34
+ // asymmetric key / certificate. `CREATE ASYMMETRIC KEY … WITH ALGORITHM = RSA_2048`
35
+ // is the distinctive form; the span between the two clauses is lazily bounded to keep
36
+ // matching linear.
37
+ const RE_MSSQL_TDE_RSA = /\bCREATE\s+ASYMMETRIC\s+KEY\b[\s\S]{0,300}?\bALGORITHM\s*=\s*RSA_\d+/gi;
20
38
 
21
39
  const RULE_PGCRYPTO: RuleMeta = {
22
40
  id: "db-pgcrypto-pubkey",
@@ -33,10 +51,25 @@ const RULE_PGCRYPTO: RuleMeta = {
33
51
  remediation:
34
52
  "Plan migration to a post-quantum KEM (ML-KEM-768) envelope for at-rest data; re-encrypt long-lived rows.",
35
53
  };
54
+ const RULE_MSSQL_TDE_RSA: RuleMeta = {
55
+ id: "db-tde-rsa",
56
+ title: "SQL Server TDE RSA key",
57
+ description: "SQL Server CREATE ASYMMETRIC KEY ... WITH ALGORITHM = RSA_* (TDE key protection)",
58
+ category: "kem",
59
+ severity: "high",
60
+ confidence: "high",
61
+ algorithm: "RSA",
62
+ hndl: true,
63
+ cwe: CWE_BROKEN_CRYPTO,
64
+ message:
65
+ "Transparent Data Encryption protects the database encryption key with a classical RSA asymmetric key; the at-rest data is harvest-now-decrypt-later exposed once the RSA key falls.",
66
+ remediation:
67
+ "Plan migration to a PQC KEM (ML-KEM-768) for TDE key protection as the database engine adds support; re-key long-lived encrypted databases.",
68
+ };
36
69
  const RULE_WEAK_SSLMODE: RuleMeta = {
37
70
  id: "db-weak-sslmode",
38
71
  title: "Database sslmode without verification",
39
- description: "libpq sslmode is disable/allow/prefer/require (no certificate verification)",
72
+ description: "libpq sslmode is allow/prefer/require (no certificate verification)",
40
73
  category: "tls",
41
74
  severity: "medium",
42
75
  confidence: "high",
@@ -51,16 +84,20 @@ const RULE_WEAK_SSLMODE: RuleMeta = {
51
84
  /** Detects classical database at-rest / transport crypto. */
52
85
  export const databaseDetector: Detector = {
53
86
  id: "database-crypto",
54
- description: "Classical crypto in database usage (pgcrypto public-key, weak client sslmode)",
87
+ description:
88
+ "Classical crypto in database usage (pgcrypto public-key, SQL Server TDE, weak client sslmode)",
55
89
  scope: "config",
56
90
  language: "any",
57
- rules: [RULE_PGCRYPTO, RULE_WEAK_SSLMODE],
91
+ rules: [RULE_PGCRYPTO, RULE_MSSQL_TDE_RSA, RULE_WEAK_SSLMODE],
58
92
  // Skip prose/docs: a README showing `sslmode=require` is not a live connection string.
59
93
  appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
60
94
  detect({ file, content }): Finding[] {
61
95
  const findings: Finding[] = [];
96
+ // Mask SQL (`--`, single-line `/* … */`), shell/ini (`#`), and ini (`;`) line
97
+ // comments: a commented `-- pgp_pub_encrypt` or `; sslmode=require` is not active.
98
+ const scan = maskCommentLines(content, ["#", "--", ";", "/*"]);
62
99
  if (file.toLowerCase().endsWith(".sql") && content.includes("pgp_pub_")) {
63
- eachMatch(RE_PGCRYPTO, content, (m) =>
100
+ eachMatch(RE_PGCRYPTO, scan, (m) =>
64
101
  findings.push(
65
102
  findingFromRule(RULE_PGCRYPTO, {
66
103
  file,
@@ -71,8 +108,11 @@ export const databaseDetector: Detector = {
71
108
  ),
72
109
  );
73
110
  }
74
- if (content.includes("sslmode")) {
75
- eachMatch(RE_WEAK_SSLMODE, content, (m) =>
111
+ const lc = content.toLowerCase();
112
+ // Cheap prefilter; matches the separators the RE_WEAK_SSLMODE regex accepts
113
+ // (`sslmode`, `ssl-mode`, `ssl_mode`) so no supported form is gated out.
114
+ if (lc.includes("sslmode") || lc.includes("ssl-mode") || lc.includes("ssl_mode")) {
115
+ eachMatch(RE_WEAK_SSLMODE, scan, (m) =>
76
116
  findings.push(
77
117
  findingFromRule(RULE_WEAK_SSLMODE, {
78
118
  file,
@@ -83,6 +123,19 @@ export const databaseDetector: Detector = {
83
123
  ),
84
124
  );
85
125
  }
126
+ // SQL Server TDE: a classical RSA asymmetric key protecting the DEK.
127
+ if (lc.includes("asymmetric key")) {
128
+ eachMatch(RE_MSSQL_TDE_RSA, scan, (m) =>
129
+ findings.push(
130
+ findingFromRule(RULE_MSSQL_TDE_RSA, {
131
+ file,
132
+ content,
133
+ index: m.index,
134
+ matchLength: m[0].length,
135
+ }),
136
+ ),
137
+ );
138
+ }
86
139
  return findings;
87
140
  },
88
141
  };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Config detector: classical DKIM email-signing keys and algorithms in DNS
3
+ * zone files, mail-server signer config (OpenDKIM), and captured
4
+ * `DKIM-Signature:` header fields (RFC 6376 + RFC 8463).
5
+ *
6
+ * DKIM (DomainKeys Identified Mail) authenticates the *origin* of email: the
7
+ * sending MTA signs selected header/body content with a private key, publishes
8
+ * the matching public key as a DNS TXT record at
9
+ * `<selector>._domainkey.<domain>`, and receivers verify the `DKIM-Signature`
10
+ * header against that published key. Like DNSSEC (see `dnssec.ts`), this is a
11
+ * pure *signature* surface: there is no confidentiality to harvest, so a DKIM
12
+ * key is NOT harvest-now-decrypt-later exposed (`hndl: false`). What breaks
13
+ * once a cryptographically-relevant quantum computer (CRQC) exists is
14
+ * unforgeability — an attacker who can derive the private key from the
15
+ * published public key can mint valid `DKIM-Signature` headers and spoof mail
16
+ * that passes DKIM/DMARC. So these findings are `category: "signature"`.
17
+ *
18
+ * Crypto surface, mapped to {@link AlgorithmFamily}:
19
+ * - `k=rsa` DNS key-type tag / `a=rsa-sha256` / `a=rsa-sha1` (DKIM-Signature
20
+ * algorithm tag) / OpenDKIM `SigningAlgorithm rsa-sha256|rsa-sha1` → RSA
21
+ * - `k=ed25519` DNS key-type tag / `a=ed25519-sha256` / OpenDKIM
22
+ * `SigningAlgorithm ed25519-sha256` (RFC 8463) → EdDSA
23
+ *
24
+ * Ed25519 DKIM (RFC 8463) is MODERN but still CLASSICAL: rotating an RSA DKIM
25
+ * key to Ed25519 shrinks the DNS record and is good hygiene, but it is NOT a
26
+ * post-quantum fix — an Ed25519 signature is just as forgeable under a CRQC.
27
+ * The remediation text says so honestly: there is no standardized PQC DKIM yet;
28
+ * the only real action is to track IETF work.
29
+ *
30
+ * Two match shapes are covered per family, both gated to a small config
31
+ * extension set AND to a file-level DKIM marker (see `hasDkimMarker`) so the
32
+ * short tag tokens (`k=rsa`, `a=rsa-sha256`) can't fire on unrelated config:
33
+ *
34
+ * 1. DNS KEY-TYPE tag — `k=rsa` / `k=ed25519` inside a published DKIM TXT
35
+ * record (`v=DKIM1; k=rsa; p=MIGf...`). Whitespace around `=` is tolerated.
36
+ * 2. SIGNING-ALGORITHM tag — the `a=` tag of a `DKIM-Signature` header
37
+ * (`a=rsa-sha256`, `a=ed25519-sha256`) and the OpenDKIM `SigningAlgorithm`
38
+ * directive (`SigningAlgorithm rsa-sha256`). The RSA variant deliberately
39
+ * also matches the legacy `rsa-sha1`.
40
+ *
41
+ * Fast reject: `detect()` bails unless the file carries a DKIM-specific marker
42
+ * (`v=DKIM1`, `_domainkey`, a `DKIM-Signature` field, an OpenDKIM
43
+ * `SigningAlgorithm` directive, or a bare `k=rsa`/`k=ed25519` key-type tag).
44
+ * This stops the generic-looking `k=rsa` / `a=rsa-sha256` tokens from firing on
45
+ * an unrelated `.conf` that happens to contain an `a=`/`k=` assignment for its
46
+ * own reasons. Prose is excluded twice over: `appliesTo` restricts to config
47
+ * extensions (README/`.md` never applies), and `detect()` re-guards against
48
+ * {@link DOC_EXTENSIONS} defensively so a docs file that slipped through a
49
+ * broader gate could never be flagged for mentioning `k=rsa` in a sentence.
50
+ */
51
+ import type { Detector, Finding, RuleMeta } from "../types.js";
52
+ import {
53
+ DOC_EXTENSIONS,
54
+ eachMatch,
55
+ findingFromRule,
56
+ hasExtension,
57
+ maskCommentLines,
58
+ } from "../detect-utils.js";
59
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
60
+
61
+ // DKIM records live in DNS zone files (`.zone`/`.db`), mail-server signer config
62
+ // (`.conf`), and captured header/TXT fixtures (`.txt`). Gated further by
63
+ // `hasDkimMarker` so the short tag tokens can't fire on unrelated config.
64
+ const DKIM_EXTENSIONS: readonly string[] = [".zone", ".db", ".conf", ".txt"];
65
+
66
+ // --- RSA (k=rsa / rsa-sha256 / rsa-sha1) ---
67
+ // DNS key-type tag: `k=rsa` in a published DKIM TXT record. Whitespace tolerated
68
+ // around `=`; case-insensitive since tags are conventionally lowercase but not
69
+ // required to be. `\b` after `rsa` keeps it from matching `rsa-...` substrings.
70
+ const RE_RSA_KEYTAG = /\bk\s*=\s*rsa\b/gi;
71
+ // DKIM-Signature `a=` tag: `a=rsa-sha256` / `a=rsa-sha1`.
72
+ const RE_RSA_ALGTAG = /\ba\s*=\s*rsa-sha(?:256|1)\b/gi;
73
+ // OpenDKIM signer directive: `SigningAlgorithm rsa-sha256` / `rsa-sha1`.
74
+ const RE_RSA_OPENDKIM = /\bSigningAlgorithm\s+rsa-sha(?:256|1)\b/gi;
75
+
76
+ // --- EdDSA (k=ed25519 / ed25519-sha256, RFC 8463) ---
77
+ const RE_EDDSA_KEYTAG = /\bk\s*=\s*ed25519\b/gi;
78
+ const RE_EDDSA_ALGTAG = /\ba\s*=\s*ed25519-sha256\b/gi;
79
+ const RE_EDDSA_OPENDKIM = /\bSigningAlgorithm\s+ed25519-sha256\b/gi;
80
+
81
+ const RULE_DKIM_RSA: RuleMeta = {
82
+ id: "dkim-rsa-key",
83
+ title: "DKIM RSA signing key/algorithm",
84
+ description:
85
+ "DKIM email signing configured with a classical RSA key/algorithm (k=rsa, a=rsa-sha256/rsa-sha1)",
86
+ category: "signature",
87
+ severity: "medium",
88
+ confidence: "high",
89
+ algorithm: "RSA",
90
+ hndl: false,
91
+ cwe: CWE_BROKEN_CRYPTO,
92
+ message:
93
+ "DKIM email signing uses a classical RSA key/algorithm (k=rsa / rsa-sha256 / rsa-sha1); DKIM-Signature headers become forgeable once a CRQC exists, allowing DKIM/DMARC-passing spoofed mail.",
94
+ remediation:
95
+ "There is no standardized post-quantum DKIM algorithm yet — track IETF work; note that rotating RSA to Ed25519 (RFC 8463) is good hygiene but NOT a PQC fix, since Ed25519 is still classical and equally forgeable under a CRQC.",
96
+ };
97
+
98
+ const RULE_DKIM_EDDSA: RuleMeta = {
99
+ id: "dkim-ed25519-key",
100
+ title: "DKIM Ed25519 signing key/algorithm",
101
+ description:
102
+ "DKIM email signing configured with a classical Ed25519 key/algorithm (k=ed25519, a=ed25519-sha256, RFC 8463)",
103
+ category: "signature",
104
+ severity: "medium",
105
+ confidence: "high",
106
+ algorithm: "EdDSA",
107
+ hndl: false,
108
+ cwe: CWE_BROKEN_CRYPTO,
109
+ message:
110
+ "DKIM email signing uses an Ed25519 key/algorithm (k=ed25519 / ed25519-sha256, RFC 8463) — modern but still classical; DKIM-Signature headers become forgeable once a CRQC exists.",
111
+ remediation:
112
+ "There is no standardized post-quantum DKIM algorithm yet — track IETF work. Ed25519 is already the smaller/modern DKIM choice but is NOT post-quantum: it stays forgeable under a CRQC, so no rotation today resolves the quantum exposure.",
113
+ };
114
+
115
+ /**
116
+ * True when `content` carries a DKIM-specific marker. Deliberately does NOT accept
117
+ * a bare `k=rsa` / `k=ed25519` tag as its own marker: that tag is exactly what the
118
+ * RSA/EdDSA rules match, so treating it as the gate would be self-satisfying and
119
+ * fire on unrelated prose ("set `k=rsa` for the legacy importer"). A real DKIM
120
+ * record always also carries `v=DKIM1`, `_domainkey`, a `DKIM-Signature` field, or
121
+ * the OpenDKIM `SigningAlgorithm` directive — one of those must be present.
122
+ */
123
+ function hasDkimMarker(content: string): boolean {
124
+ return (
125
+ /\bDKIM1\b/i.test(content) ||
126
+ content.includes("_domainkey") ||
127
+ /DKIM-Signature/i.test(content) ||
128
+ /\bSigningAlgorithm\b/i.test(content)
129
+ );
130
+ }
131
+
132
+ interface DkimRule {
133
+ meta: RuleMeta;
134
+ res: readonly RegExp[];
135
+ }
136
+
137
+ const DKIM_RULES: readonly DkimRule[] = [
138
+ { meta: RULE_DKIM_RSA, res: [RE_RSA_KEYTAG, RE_RSA_ALGTAG, RE_RSA_OPENDKIM] },
139
+ { meta: RULE_DKIM_EDDSA, res: [RE_EDDSA_KEYTAG, RE_EDDSA_ALGTAG, RE_EDDSA_OPENDKIM] },
140
+ ];
141
+
142
+ /** Detects classical DKIM signing keys/algorithms in zone files and mail signer config. */
143
+ export const dkimDetector: Detector = {
144
+ id: "dkim-crypto",
145
+ description: "Classical DKIM email signing keys/algorithms in zone files / mail signer config",
146
+ scope: "config",
147
+ language: "any",
148
+ rules: DKIM_RULES.map((r) => r.meta),
149
+ appliesTo: (f) => hasExtension(f, DKIM_EXTENSIONS) && !hasExtension(f, DOC_EXTENSIONS),
150
+ detect({ file, content }): Finding[] {
151
+ // Defensive doc guard: even if a broader gate ever let a prose file through,
152
+ // a README mentioning `k=rsa` in a sentence must never be flagged.
153
+ if (hasExtension(file, DOC_EXTENSIONS)) return [];
154
+ if (!hasDkimMarker(content)) return [];
155
+
156
+ // Mask whole comment lines so a commented-out record can't fire: zone files
157
+ // use `;`, mail/OpenDKIM config uses `#` and `//`. Offsets are preserved so
158
+ // finding line/column/snippet stay exact for the live lines that remain.
159
+ const scan = maskCommentLines(content, [";", "#", "//"]);
160
+ const findings: Finding[] = [];
161
+ for (const { meta, res } of DKIM_RULES) {
162
+ for (const re of res) {
163
+ eachMatch(re, scan, (m) =>
164
+ findings.push(
165
+ findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
166
+ ),
167
+ );
168
+ }
169
+ }
170
+ return findings;
171
+ },
172
+ };
@@ -50,7 +50,7 @@
50
50
  * mention `ED25519` or `DSA` for its own reasons.
51
51
  */
52
52
  import type { Detector, Finding, RuleMeta } from "../types.js";
53
- import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
53
+ import { eachMatch, findingFromRule, hasExtension, maskCommentLines } from "../detect-utils.js";
54
54
  import { CWE_BROKEN_CRYPTO } from "../cwe.js";
55
55
 
56
56
  const DNSSEC_EXTENSIONS: readonly string[] = [".zone", ".db", ".conf"];
@@ -62,19 +62,24 @@ const NUM_EDDSA = "15|16"; // ED25519, ED448
62
62
  const NUM_DSA = "3|6"; // DSA, DSA-NSEC3-SHA1
63
63
 
64
64
  // --- NAMED form: mnemonic algorithm names in signer/policy config or CLI args. ---
65
- const RE_NAMED_RSA = /\bRSASHA(?:256|512|1(?:-NSEC3-SHA1)?)\b/g;
66
- const RE_NAMED_ECDSA = /\bECDSAP(?:256SHA256|384SHA384)\b/g;
67
- const RE_NAMED_EDDSA = /\bED(?:25519|448)\b/g;
65
+ // Case-insensitive: BIND `dnssec-policy` and Knot config conventionally write the
66
+ // mnemonics in lowercase (`algorithm rsasha256;`, `algorithm ed25519;`), so a
67
+ // case-sensitive match would miss the primary real-world signer-config syntax. Still
68
+ // gated by `hasDnssecMarker` + the DNSSEC extensions, so generic tokens can't fire.
69
+ const RE_NAMED_RSA = /\bRSASHA(?:256|512|1(?:-NSEC3-SHA1)?)\b/gi;
70
+ const RE_NAMED_ECDSA = /\bECDSAP(?:256SHA256|384SHA384)\b/gi;
71
+ const RE_NAMED_EDDSA = /\bED(?:25519|448)\b/gi;
68
72
  // Bare "DSA" is too generic to key off alone; require it right after an
69
73
  // `algorithm` keyword (Knot/BIND config / dnssec-policy style), optionally
70
74
  // quoted or separated by `:`/`=`.
71
75
  const RE_NAMED_DSA = /\balgorithm\s*[:=]?\s*"?DSA(?:-NSEC3-SHA1)?"?\b/gi;
72
76
 
73
- // --- STRUCTURAL form: `DNSKEY <flags> 3 <algorithm>` presentation RDATA (RFC 4034 §2). ---
74
- const RE_DNSKEY_RSA = new RegExp(`\\bDNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_RSA})\\b`, "g");
75
- const RE_DNSKEY_ECDSA = new RegExp(`\\bDNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_ECDSA})\\b`, "g");
76
- const RE_DNSKEY_EDDSA = new RegExp(`\\bDNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_EDDSA})\\b`, "g");
77
- const RE_DNSKEY_DSA = new RegExp(`\\bDNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_DSA})\\b`, "g");
77
+ // --- STRUCTURAL form: `[C]DNSKEY <flags> 3 <algorithm>` presentation RDATA (RFC 4034
78
+ // §2). `C?` also matches CDNSKEY (child-zone key with identical RDATA, RFC 7344). ---
79
+ const RE_DNSKEY_RSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_RSA})\\b`, "g");
80
+ const RE_DNSKEY_ECDSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_ECDSA})\\b`, "g");
81
+ const RE_DNSKEY_EDDSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_EDDSA})\\b`, "g");
82
+ const RE_DNSKEY_DSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_DSA})\\b`, "g");
78
83
 
79
84
  const RULE_DNSSEC_RSA: RuleMeta = {
80
85
  id: "dnssec-rsa-sig",
@@ -171,10 +176,14 @@ export const dnssecDetector: Detector = {
171
176
  detect({ file, content }): Finding[] {
172
177
  if (!hasDnssecMarker(content)) return [];
173
178
 
179
+ // Mask whole comment lines so a commented-out algorithm can't fire: zone files
180
+ // use `;`, named.conf uses `#` and `//`. These extensions are not centrally
181
+ // stripped; offsets are preserved so finding locations stay exact.
182
+ const scan = maskCommentLines(content, [";", "#", "//"]);
174
183
  const findings: Finding[] = [];
175
184
  for (const { meta, res } of DNSSEC_RULES) {
176
185
  for (const re of res) {
177
- eachMatch(re, content, (m) =>
186
+ eachMatch(re, scan, (m) =>
178
187
  findings.push(
179
188
  findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
180
189
  ),
@@ -19,6 +19,10 @@ import { ELIXIR_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../
19
19
  import { CWE_BROKEN_CRYPTO } from "../cwe.js";
20
20
 
21
21
  const RE_EX_GEN = /:crypto\.generate_key\s*\(\s*:(\w+)/g;
22
+ // `:crypto.compute_key(:ecdh|:dh, PeerPub, MyKey, :curve)` is the DH/ECDH AGREEMENT
23
+ // operation itself — the harvest-now-decrypt-later step — which generate_key alone
24
+ // misses. Classified by the same type/curve logic as generate_key.
25
+ const RE_EX_COMPUTE = /:crypto\.compute_key\s*\(\s*:(\w+)/g;
22
26
  const RE_EX_SIGN = /:crypto\.(?:sign|verify)\s*\(\s*:(\w+)/g;
23
27
  const RE_EX_X509_RSA = /\bX509\.PrivateKey\.new_rsa\s*\(/g;
24
28
  const RE_EX_X509_EC = /\bX509\.PrivateKey\.new_ec\s*\(/g;
@@ -146,6 +150,7 @@ function classifySign(type: string): AlgorithmFamily | null {
146
150
  if (type === "rsa") return "RSA";
147
151
  if (type === "ecdsa") return "ECDSA";
148
152
  if (type === "eddsa" || type === "ed25519") return "EdDSA";
153
+ if (type === "dss") return "DSA"; // Erlang's algorithm atom for DSA signatures
149
154
  return null;
150
155
  }
151
156
 
@@ -183,6 +188,23 @@ export const elixirDetector: Detector = {
183
188
  );
184
189
  });
185
190
 
191
+ // :crypto.compute_key(:type, …) — the (EC)DH agreement operation.
192
+ eachMatch(RE_EX_COMPUTE, content, (m) => {
193
+ const cls = classifyGen(m[1], content.slice(m.index, m.index + 120));
194
+ if (!cls) return;
195
+ findings.push(
196
+ findingFromRule(RULE_EX_KEYGEN, at(m), {
197
+ title: `Elixir :crypto ${cls.label} key agreement`,
198
+ category: cls.cat,
199
+ severity: cls.sev,
200
+ algorithm: cls.algo,
201
+ hndl: cls.hndl,
202
+ message: `Performs classical ${cls.label} key agreement via Erlang :crypto (Elixir) — harvest-now-decrypt-later exposed.`,
203
+ ...(cls.remediation ? { remediation: cls.remediation } : {}),
204
+ }),
205
+ );
206
+ });
207
+
186
208
  // :crypto.sign / :crypto.verify(:type, …).
187
209
  eachMatch(RE_EX_SIGN, content, (m) => {
188
210
  const algo = classifySign(m[1]);
@@ -213,11 +235,20 @@ export const elixirDetector: Detector = {
213
235
  ),
214
236
  );
215
237
 
216
- // JOSE.JWK.generate_key({:rsa|:ec|:okp, …}).
238
+ // JOSE.JWK.generate_key({:rsa|:ec|:okp, …}). An OKP key is EdDSA (Ed25519/Ed448,
239
+ // signature) OR a Montgomery key-agreement key (X25519/X448, HNDL) — inspect the
240
+ // curve atom in a short window so an X25519 OKP is not mislabeled as an EdDSA
241
+ // signature.
217
242
  eachMatch(RE_EX_JOSE, content, (m) => {
218
243
  const kind = m[1];
244
+ let okpCls: Cls = EDDSA_CLS;
245
+ if (kind === "okp") {
246
+ const window = content.slice(m.index, m.index + 80);
247
+ if (/:x25519\b/i.test(window)) okpCls = X25519_CLS;
248
+ else if (/:x448\b/i.test(window)) okpCls = X448_CLS;
249
+ }
219
250
  const cls =
220
- kind === "rsa" ? RSA_CLS : kind === "ec" ? EC_CLS : kind === "okp" ? EDDSA_CLS : null;
251
+ kind === "rsa" ? RSA_CLS : kind === "ec" ? EC_CLS : kind === "okp" ? okpCls : null;
221
252
  if (!cls) return;
222
253
  findings.push(
223
254
  findingFromRule(RULE_EX_JOSE, at(m), {