@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
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Config detector: classical DNSSEC signing algorithms in zone files and
3
+ * signer/resolver config (BIND, Knot DNS, PowerDNS, `ldns-signzone` /
4
+ * `dnssec-signzone` invocations recorded in ops config).
5
+ *
6
+ * DNSSEC is "communication between things" infrastructure: resolvers verify
7
+ * DNSKEY/RRSIG chains to authenticate zone data. Unlike a KEM or key-exchange
8
+ * finding, a DNSSEC signing key is not harvest-now-decrypt-later exposed —
9
+ * there is no confidentiality to harvest — but every classical signature it
10
+ * produces becomes FORGEABLE the moment a cryptographically-relevant quantum
11
+ * computer (CRQC) exists. A forged DNSKEY/RRSIG chain lets an attacker spoof
12
+ * DNSSEC-validated records (cache poisoning that a validating resolver would
13
+ * otherwise reject). So these findings are `category: "signature"`,
14
+ * `hndl: false`.
15
+ *
16
+ * RFC 8624 / IANA "DNSSEC Algorithm Numbers" families covered, mapped to
17
+ * {@link AlgorithmFamily}:
18
+ * - RSASHA1 (5), RSASHA1-NSEC3-SHA1 (7), RSASHA256 (8), RSASHA512 (10) → RSA
19
+ * - ECDSAP256SHA256 (13), ECDSAP384SHA384 (14) → ECDSA
20
+ * - ED25519 (15), ED448 (16) → EdDSA
21
+ * - DSA (3), DSA-NSEC3-SHA1 (6) → DSA
22
+ *
23
+ * Two independent match shapes per family, both gated to `.zone` / `.db` /
24
+ * `.conf` files AND to a file-level DNSSEC marker (see `hasDnssecMarker`
25
+ * below) so the algorithm tokens can't fire on unrelated config:
26
+ *
27
+ * 1. NAMED form — the algorithm's mnemonic name, as written in signer/policy
28
+ * config (Knot `algorithm RSASHA256;`, a `dnssec-policy` block, BIND
29
+ * `algorithm: ecdsap256sha256;`) or on an `ldns-signzone -a ECDSAP256SHA256`
30
+ * / `dnssec-signzone -a ECDSAP256SHA256` command line. These mnemonics
31
+ * (`RSASHA256`, `ECDSAP384SHA384`, `ED25519`, …) are distinctive enough to
32
+ * match bare, EXCEPT plain `DSA`, which is too generic a token on its own
33
+ * (used well outside DNSSEC); that one is only matched immediately after
34
+ * an `algorithm` keyword.
35
+ * 2. STRUCTURAL form — the DNSKEY presentation-format RDATA, e.g.
36
+ * `example.com. IN DNSKEY 257 3 8 AwEAAd...`, where the fields after the
37
+ * `DNSKEY` keyword are `flags protocol algorithm public-key` (RFC 4034
38
+ * §2). Anchoring on the literal `DNSKEY` keyword AND the literal
39
+ * protocol value `3` (always 3 for DNSSEC) before a restricted algorithm
40
+ * number keeps this precise despite being numeric. Bare algorithm
41
+ * numbers with no such anchor are deliberately NOT matched anywhere —
42
+ * `algorithm 8;` alone is far too ambiguous to key off of.
43
+ *
44
+ * Fast reject: `detect()` bails unless the file contains `DNSKEY`, `RRSIG`,
45
+ * a case-insensitive `dnssec` substring (present in `dnssec-policy`,
46
+ * `dnssec-signing: on`, etc. in any real signer config), or an
47
+ * `ldns-signzone` / `dnssec-signzone` invocation. This stops the DSA/EdDSA
48
+ * named-form tokens — which are individually generic outside DNSSEC — from
49
+ * firing on, say, an unrelated SSH or TLS `.conf` file that happens to
50
+ * mention `ED25519` or `DSA` for its own reasons.
51
+ */
52
+ import type { Detector, Finding, RuleMeta } from "../types.js";
53
+ import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
54
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
55
+
56
+ const DNSSEC_EXTENSIONS: readonly string[] = [".zone", ".db", ".conf"];
57
+
58
+ // IANA DNSSEC algorithm numbers, grouped by family.
59
+ const NUM_RSA = "5|7|8|10"; // RSASHA1, RSASHA1-NSEC3-SHA1, RSASHA256, RSASHA512
60
+ const NUM_ECDSA = "13|14"; // ECDSAP256SHA256, ECDSAP384SHA384
61
+ const NUM_EDDSA = "15|16"; // ED25519, ED448
62
+ const NUM_DSA = "3|6"; // DSA, DSA-NSEC3-SHA1
63
+
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;
68
+ // Bare "DSA" is too generic to key off alone; require it right after an
69
+ // `algorithm` keyword (Knot/BIND config / dnssec-policy style), optionally
70
+ // quoted or separated by `:`/`=`.
71
+ const RE_NAMED_DSA = /\balgorithm\s*[:=]?\s*"?DSA(?:-NSEC3-SHA1)?"?\b/gi;
72
+
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");
78
+
79
+ const RULE_DNSSEC_RSA: RuleMeta = {
80
+ id: "dnssec-rsa-sig",
81
+ title: "DNSSEC RSA signing algorithm",
82
+ description: "DNSSEC zone signed with a classical RSA algorithm (RSASHA1/256/512)",
83
+ category: "signature",
84
+ severity: "medium",
85
+ confidence: "high",
86
+ algorithm: "RSA",
87
+ hndl: false,
88
+ cwe: CWE_BROKEN_CRYPTO,
89
+ message:
90
+ "DNSSEC zone is signed with a classical RSA algorithm (RSASHA1/RSASHA1-NSEC3-SHA1/RSASHA256/RSASHA512); DNSKEY/RRSIG signatures become forgeable once a CRQC exists.",
91
+ remediation:
92
+ "Track IETF dnsop post-quantum DNSSEC signing work (ML-DSA); plan re-signing with a PQC algorithm once assigned an IANA DNSSEC algorithm number.",
93
+ };
94
+ const RULE_DNSSEC_ECDSA: RuleMeta = {
95
+ id: "dnssec-ecdsa-sig",
96
+ title: "DNSSEC ECDSA signing algorithm",
97
+ description: "DNSSEC zone signed with a classical ECDSA algorithm (ECDSAP256SHA256/384SHA384)",
98
+ category: "signature",
99
+ severity: "medium",
100
+ confidence: "high",
101
+ algorithm: "ECDSA",
102
+ hndl: false,
103
+ cwe: CWE_BROKEN_CRYPTO,
104
+ message:
105
+ "DNSSEC zone is signed with a classical ECDSA algorithm (ECDSAP256SHA256/ECDSAP384SHA384); DNSKEY/RRSIG signatures become forgeable once a CRQC exists.",
106
+ remediation:
107
+ "Track IETF dnsop post-quantum DNSSEC signing work (ML-DSA); plan re-signing with a PQC algorithm once assigned an IANA DNSSEC algorithm number.",
108
+ };
109
+ const RULE_DNSSEC_EDDSA: RuleMeta = {
110
+ id: "dnssec-eddsa-sig",
111
+ title: "DNSSEC EdDSA signing algorithm",
112
+ description: "DNSSEC zone signed with a classical EdDSA algorithm (ED25519/ED448)",
113
+ category: "signature",
114
+ severity: "medium",
115
+ confidence: "high",
116
+ algorithm: "EdDSA",
117
+ hndl: false,
118
+ cwe: CWE_BROKEN_CRYPTO,
119
+ message:
120
+ "DNSSEC zone is signed with a classical EdDSA algorithm (ED25519/ED448); modern but still classical — DNSKEY/RRSIG signatures become forgeable once a CRQC exists.",
121
+ remediation:
122
+ "Track IETF dnsop post-quantum DNSSEC signing work (ML-DSA); plan re-signing with a PQC algorithm once assigned an IANA DNSSEC algorithm number.",
123
+ };
124
+ const RULE_DNSSEC_DSA: RuleMeta = {
125
+ id: "dnssec-dsa-sig",
126
+ title: "DNSSEC DSA signing algorithm (deprecated)",
127
+ description:
128
+ "DNSSEC zone signed with the deprecated classical DSA algorithm (DSA/DSA-NSEC3-SHA1)",
129
+ category: "signature",
130
+ severity: "medium",
131
+ confidence: "high",
132
+ algorithm: "DSA",
133
+ hndl: false,
134
+ cwe: CWE_BROKEN_CRYPTO,
135
+ message:
136
+ "DNSSEC zone is signed with DSA/DSA-NSEC3-SHA1 — deprecated by RFC 8624 (MUST NOT sign) and, independent of that, forgeable once a CRQC exists.",
137
+ remediation:
138
+ "Re-sign with a non-deprecated algorithm today (RFC 8624); track IETF dnsop post-quantum DNSSEC signing work (ML-DSA) for the eventual PQC migration.",
139
+ };
140
+
141
+ /** True when `content` carries some DNSSEC-specific marker (not just a bare algorithm token). */
142
+ function hasDnssecMarker(content: string): boolean {
143
+ return (
144
+ content.includes("DNSKEY") ||
145
+ content.includes("RRSIG") ||
146
+ /dnssec/i.test(content) ||
147
+ /ldns-signzone|dnssec-signzone/.test(content)
148
+ );
149
+ }
150
+
151
+ interface DnssecRule {
152
+ meta: RuleMeta;
153
+ res: readonly RegExp[];
154
+ }
155
+
156
+ const DNSSEC_RULES: readonly DnssecRule[] = [
157
+ { meta: RULE_DNSSEC_RSA, res: [RE_NAMED_RSA, RE_DNSKEY_RSA] },
158
+ { meta: RULE_DNSSEC_ECDSA, res: [RE_NAMED_ECDSA, RE_DNSKEY_ECDSA] },
159
+ { meta: RULE_DNSSEC_EDDSA, res: [RE_NAMED_EDDSA, RE_DNSKEY_EDDSA] },
160
+ { meta: RULE_DNSSEC_DSA, res: [RE_NAMED_DSA, RE_DNSKEY_DSA] },
161
+ ];
162
+
163
+ /** Detects classical DNSSEC signing algorithms in zone files and signer config. */
164
+ export const dnssecDetector: Detector = {
165
+ id: "dnssec-crypto",
166
+ description: "Classical DNSSEC signing algorithms in zone files / signer config",
167
+ scope: "config",
168
+ language: "any",
169
+ rules: DNSSEC_RULES.map((r) => r.meta),
170
+ appliesTo: (f) => hasExtension(f, DNSSEC_EXTENSIONS),
171
+ detect({ file, content }): Finding[] {
172
+ if (!hasDnssecMarker(content)) return [];
173
+
174
+ const findings: Finding[] = [];
175
+ for (const { meta, res } of DNSSEC_RULES) {
176
+ for (const re of res) {
177
+ eachMatch(re, content, (m) =>
178
+ findings.push(
179
+ findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
180
+ ),
181
+ );
182
+ }
183
+ }
184
+ return findings;
185
+ },
186
+ };
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Source-code detector for classical asymmetric cryptography in Elixir (the BEAM
3
+ * / Phoenix ecosystem), previously uncovered. Handles the three common surfaces:
4
+ *
5
+ * - **Erlang `:crypto`** — `:crypto.generate_key(:rsa|:ecdh|:dh|:eddsa, …)`
6
+ * (the curve atom disambiguates X25519/X448 from a generic ECDH curve) and
7
+ * `:crypto.sign/:crypto.verify(:rsa|:ecdsa|:eddsa, …)`.
8
+ * - **`X509`** (the `x509` hex package) — `X509.PrivateKey.new_rsa` / `new_ec`.
9
+ * - **`JOSE`** (erlang-jose) — `JOSE.JWK.generate_key({:rsa|:ec|:okp, …})`.
10
+ *
11
+ * Lexical, like the other packs; the `:crypto.` atom-module syntax and the
12
+ * `X509.` / `JOSE.` module paths are distinctive, so the false-positive rate is low.
13
+ *
14
+ * HNDL: RSA and (EC/X)DH key agreement are harvest-now-decrypt-later exposed
15
+ * (hndl:true); ECDSA / EdDSA signatures are hndl:false but forgeable.
16
+ */
17
+ import type { AlgorithmFamily, Detector, Finding, RuleMeta } from "../types.js";
18
+ import { ELIXIR_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
19
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
20
+
21
+ const RE_EX_GEN = /:crypto\.generate_key\s*\(\s*:(\w+)/g;
22
+ const RE_EX_SIGN = /:crypto\.(?:sign|verify)\s*\(\s*:(\w+)/g;
23
+ const RE_EX_X509_RSA = /\bX509\.PrivateKey\.new_rsa\s*\(/g;
24
+ const RE_EX_X509_EC = /\bX509\.PrivateKey\.new_ec\s*\(/g;
25
+ const RE_EX_JOSE = /\bJOSE\.JWK\.generate_key\s*\(\s*\{\s*:(\w+)/g;
26
+
27
+ interface Cls {
28
+ algo: AlgorithmFamily;
29
+ cat: Finding["category"];
30
+ sev: Finding["severity"];
31
+ hndl: boolean;
32
+ label: string;
33
+ remediation?: string;
34
+ }
35
+ const SIG_REM = "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)";
36
+ const KEX_REM = "hybrid X25519MLKEM768 (ML-KEM-768)";
37
+
38
+ const RULE_EX_KEYGEN: RuleMeta = {
39
+ id: "elixir-crypto-keygen",
40
+ title: "Elixir :crypto key generation",
41
+ description: ":crypto.generate_key (rsa/ecdh/dh/eddsa)",
42
+ category: "kem",
43
+ severity: "high",
44
+ confidence: "high",
45
+ algorithm: "RSA",
46
+ hndl: true,
47
+ cwe: CWE_BROKEN_CRYPTO,
48
+ message: "Generates a classical key pair via Erlang :crypto (Elixir) — not quantum-safe.",
49
+ };
50
+ const RULE_EX_SIGN: RuleMeta = {
51
+ id: "elixir-crypto-sign",
52
+ title: "Elixir :crypto signature",
53
+ description: ":crypto.sign / :crypto.verify (rsa/ecdsa/eddsa)",
54
+ category: "signature",
55
+ severity: "high",
56
+ confidence: "high",
57
+ algorithm: "unknown",
58
+ hndl: false,
59
+ cwe: CWE_BROKEN_CRYPTO,
60
+ message: "Classical signature via Erlang :crypto (Elixir) is forgeable by a quantum attacker.",
61
+ remediation: SIG_REM,
62
+ };
63
+ const RULE_EX_X509: RuleMeta = {
64
+ id: "elixir-x509-keygen",
65
+ title: "Elixir X509 key generation",
66
+ description: "X509.PrivateKey.new_rsa / new_ec",
67
+ category: "kem",
68
+ severity: "high",
69
+ confidence: "high",
70
+ algorithm: "RSA",
71
+ hndl: true,
72
+ cwe: CWE_BROKEN_CRYPTO,
73
+ message: "Generates a classical key pair via the X509 library (Elixir) — not quantum-safe.",
74
+ };
75
+ const RULE_EX_JOSE: RuleMeta = {
76
+ id: "elixir-jose-jwk",
77
+ title: "Elixir JOSE JWK generation",
78
+ description: "JOSE.JWK.generate_key ({:rsa|:ec|:okp})",
79
+ category: "kem",
80
+ severity: "high",
81
+ confidence: "high",
82
+ algorithm: "RSA",
83
+ hndl: true,
84
+ cwe: CWE_BROKEN_CRYPTO,
85
+ message: "Generates a classical JWK via JOSE (Elixir) — not quantum-safe.",
86
+ };
87
+
88
+ const RSA_CLS: Cls = { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" };
89
+ const DH_CLS: Cls = { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "DH" };
90
+ const ECDH_CLS: Cls = {
91
+ algo: "ECDH",
92
+ cat: "key-exchange",
93
+ sev: "high",
94
+ hndl: true,
95
+ label: "ECDH",
96
+ remediation: KEX_REM,
97
+ };
98
+ const X25519_CLS: Cls = {
99
+ algo: "X25519",
100
+ cat: "key-exchange",
101
+ sev: "medium",
102
+ hndl: true,
103
+ label: "X25519",
104
+ remediation: KEX_REM,
105
+ };
106
+ const X448_CLS: Cls = { ...X25519_CLS, algo: "X448", label: "X448" };
107
+ const EDDSA_CLS: Cls = {
108
+ algo: "EdDSA",
109
+ cat: "signature",
110
+ sev: "low",
111
+ hndl: false,
112
+ label: "EdDSA",
113
+ remediation: SIG_REM,
114
+ };
115
+ const EC_CLS: Cls = {
116
+ algo: "ECDH",
117
+ cat: "key-exchange",
118
+ sev: "high",
119
+ hndl: true,
120
+ label: "EC (ECDSA/ECDH)",
121
+ remediation: KEX_REM,
122
+ };
123
+
124
+ /** Classify a `:crypto.generate_key(:<type>, …)` by its type atom (+ the curve
125
+ * atom in a short window for the ecdh X25519/X448 case). */
126
+ function classifyGen(type: string, window: string): Cls | null {
127
+ switch (type) {
128
+ case "rsa":
129
+ return RSA_CLS;
130
+ case "dh":
131
+ return DH_CLS;
132
+ case "eddsa":
133
+ case "ed25519":
134
+ return EDDSA_CLS;
135
+ case "ecdh":
136
+ if (/:x25519\b/i.test(window)) return X25519_CLS;
137
+ if (/:x448\b/i.test(window)) return X448_CLS;
138
+ return ECDH_CLS;
139
+ default:
140
+ return null; // srp / other non-broken-in-this-model types
141
+ }
142
+ }
143
+
144
+ /** Signature algorithm classification for `:crypto.sign(:<type>, …)`. */
145
+ function classifySign(type: string): AlgorithmFamily | null {
146
+ if (type === "rsa") return "RSA";
147
+ if (type === "ecdsa") return "ECDSA";
148
+ if (type === "eddsa" || type === "ed25519") return "EdDSA";
149
+ return null;
150
+ }
151
+
152
+ /** Detects classical asymmetric crypto in Elixir (:crypto, X509, JOSE). */
153
+ export const elixirDetector: Detector = {
154
+ id: "elixir-crypto",
155
+ description: "Classical asymmetric crypto in Elixir (:crypto, X509, JOSE)",
156
+ scope: "source",
157
+ language: "elixir",
158
+ rules: [RULE_EX_KEYGEN, RULE_EX_SIGN, RULE_EX_X509, RULE_EX_JOSE],
159
+ appliesTo: (f) => hasExtension(f, ELIXIR_EXTENSIONS),
160
+ detect({ file, content }): Finding[] {
161
+ const findings: Finding[] = [];
162
+ const at = (m: RegExpExecArray) => ({
163
+ file,
164
+ content,
165
+ index: m.index,
166
+ matchLength: m[0].length,
167
+ });
168
+
169
+ // :crypto.generate_key(:type, …) — classify by the type atom (+ curve window).
170
+ eachMatch(RE_EX_GEN, content, (m) => {
171
+ const cls = classifyGen(m[1], content.slice(m.index, m.index + 80));
172
+ if (!cls) return;
173
+ findings.push(
174
+ findingFromRule(RULE_EX_KEYGEN, at(m), {
175
+ title: `Elixir :crypto ${cls.label} key generation`,
176
+ category: cls.cat,
177
+ severity: cls.sev,
178
+ algorithm: cls.algo,
179
+ hndl: cls.hndl,
180
+ message: `Generates a classical ${cls.label} key pair via Erlang :crypto (Elixir) — not quantum-safe.`,
181
+ ...(cls.remediation ? { remediation: cls.remediation } : {}),
182
+ }),
183
+ );
184
+ });
185
+
186
+ // :crypto.sign / :crypto.verify(:type, …).
187
+ eachMatch(RE_EX_SIGN, content, (m) => {
188
+ const algo = classifySign(m[1]);
189
+ if (!algo) return;
190
+ findings.push(
191
+ findingFromRule(RULE_EX_SIGN, at(m), {
192
+ algorithm: algo,
193
+ message: `Classical ${algo} signature via Erlang :crypto (Elixir) is forgeable by a quantum attacker.`,
194
+ }),
195
+ );
196
+ });
197
+
198
+ // X509.PrivateKey.new_rsa / new_ec.
199
+ eachMatch(RE_EX_X509_RSA, content, (m) =>
200
+ findings.push(findingFromRule(RULE_EX_X509, at(m), { algorithm: "RSA" })),
201
+ );
202
+ eachMatch(RE_EX_X509_EC, content, (m) =>
203
+ findings.push(
204
+ findingFromRule(RULE_EX_X509, at(m), {
205
+ title: "Elixir X509 EC key generation",
206
+ category: EC_CLS.cat,
207
+ algorithm: EC_CLS.algo,
208
+ hndl: EC_CLS.hndl,
209
+ message:
210
+ "Generates a classical EC key pair via the X509 library (Elixir); EC keys feed BOTH ECDSA and ECDH.",
211
+ remediation: KEX_REM,
212
+ }),
213
+ ),
214
+ );
215
+
216
+ // JOSE.JWK.generate_key({:rsa|:ec|:okp, …}).
217
+ eachMatch(RE_EX_JOSE, content, (m) => {
218
+ const kind = m[1];
219
+ const cls =
220
+ kind === "rsa" ? RSA_CLS : kind === "ec" ? EC_CLS : kind === "okp" ? EDDSA_CLS : null;
221
+ if (!cls) return;
222
+ findings.push(
223
+ findingFromRule(RULE_EX_JOSE, at(m), {
224
+ title: `Elixir JOSE ${cls.label} JWK`,
225
+ category: cls.cat,
226
+ severity: cls.sev,
227
+ algorithm: cls.algo,
228
+ hndl: cls.hndl,
229
+ message: `Generates a classical ${cls.label} JWK via JOSE (Elixir) — not quantum-safe.`,
230
+ ...(cls.remediation ? { remediation: cls.remediation } : {}),
231
+ }),
232
+ );
233
+ });
234
+
235
+ return findings;
236
+ },
237
+ };