@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
package/src/comments.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { Finding } from "./types.js";
18
18
 
19
- export type CommentStyle = "c" | "hash";
19
+ type CommentStyle = "c" | "hash";
20
20
 
21
21
  /** C-style (line + block) comment languages, by extension. */
22
22
  const C_LIKE: readonly string[] = [
@@ -41,45 +41,96 @@ const C_LIKE: readonly string[] = [
41
41
  ".cxx",
42
42
  ".hpp",
43
43
  ".hh",
44
+ // PHP and Scala also use C-style `//` + `/* */` comments (PHP additionally uses
45
+ // `#`, handled by the hash lexer running first would miss `//`; C-style covers both
46
+ // since PHP `//` is the common form and `#` lines are rare in modern PHP).
47
+ ".php",
48
+ ".php3",
49
+ ".php4",
50
+ ".php5",
51
+ ".phtml",
52
+ ".scala",
53
+ ".sc",
54
+ ".swift",
55
+ // Objective-C (.m/.mm) and Dart (.dart) both use C-style `//` + `/* */`.
56
+ ".m",
57
+ ".mm",
58
+ ".dart",
59
+ // Smart-contract languages (Solidity/Move/Cairo) also use C-style comments.
60
+ ".sol",
61
+ ".move",
62
+ ".cairo",
44
63
  ];
45
64
 
46
65
  /** Hash-style (`#`) comment languages, by extension. */
47
- const HASH_LIKE: readonly string[] = [".py", ".pyi", ".pyw", ".rb"];
66
+ const HASH_LIKE: readonly string[] = [".py", ".pyi", ".pyw", ".rb", ".ex", ".exs"];
48
67
 
49
68
  /** The comment style for a file path, or null when we don't strip comments for it. */
50
- export function commentStyleForFile(file: string): CommentStyle | null {
69
+ function commentStyleForFile(file: string): CommentStyle | null {
51
70
  const lower = file.toLowerCase();
52
71
  if (C_LIKE.some((e) => lower.endsWith(e))) return "c";
53
72
  if (HASH_LIKE.some((e) => lower.endsWith(e))) return "hash";
54
73
  return null;
55
74
  }
56
75
 
76
+ /**
77
+ * Advance past a string / char literal starting at `content[i]` (a quote char);
78
+ * returns the index just after it. Two correctness rules a naive escape-scan got
79
+ * wrong (both verified as real bugs):
80
+ * - a `'` char literal must close within a short window (`'\u{10FFFF}'` is ~11
81
+ * chars); otherwise it is a Rust lifetime (`'a`) or a stray apostrophe, NOT a
82
+ * literal — return `i + 1` so the scan continues instead of consuming the
83
+ * rest of the file.
84
+ * - Go raw strings (backtick, when `rawBacktick`) do NOT process escapes, so a
85
+ * trailing backslash (`` `C:\` ``) must not swallow the closing delimiter.
86
+ */
87
+ function skipQuoted(content: string, i: number, n: number, rawBacktick: boolean): number {
88
+ const quote = content[i];
89
+ if (quote === "'") {
90
+ const limit = Math.min(n, i + 1 + 12);
91
+ let j = i + 1;
92
+ while (j < limit) {
93
+ if (content[j] === "\\") {
94
+ j += 2;
95
+ continue;
96
+ }
97
+ if (content[j] === "'") return j + 1;
98
+ j++;
99
+ }
100
+ return i + 1; // a lifetime / stray apostrophe, not a char literal
101
+ }
102
+ const escapes = !(quote === "`" && rawBacktick);
103
+ let j = i + 1;
104
+ while (j < n) {
105
+ if (escapes && content[j] === "\\") {
106
+ j += 2;
107
+ continue;
108
+ }
109
+ if (content[j] === quote) return j + 1;
110
+ j++;
111
+ }
112
+ return n;
113
+ }
114
+
57
115
  /**
58
116
  * Compute the comment spans (`[start, end)` offsets) of `content`, skipping over
59
117
  * string literals so a comment marker inside a string is not treated as a
60
- * comment. Spans are returned sorted and non-overlapping.
118
+ * comment. Spans are returned sorted and non-overlapping. `rawBacktick` (Go)
119
+ * makes backtick strings raw (no escapes).
61
120
  */
62
- export function commentSpans(content: string, style: CommentStyle): Array<[number, number]> {
121
+ export function commentSpans(
122
+ content: string,
123
+ style: CommentStyle,
124
+ rawBacktick = false,
125
+ ): Array<[number, number]> {
63
126
  const spans: Array<[number, number]> = [];
64
127
  const n = content.length;
65
128
  let i = 0;
66
129
  while (i < n) {
67
130
  const c = content[i];
68
- // String / template literal: skip to the matching unescaped delimiter.
131
+ // String / char literal: skip past it (lexically correct for lifetimes + raw strings).
69
132
  if (c === '"' || c === "'" || c === "`") {
70
- const quote = c;
71
- i++;
72
- while (i < n) {
73
- if (content[i] === "\\") {
74
- i += 2;
75
- continue;
76
- }
77
- if (content[i] === quote) {
78
- i++;
79
- break;
80
- }
81
- i++;
82
- }
133
+ i = skipQuoted(content, i, n, rawBacktick);
83
134
  continue;
84
135
  }
85
136
  if (style === "c" && c === "/" && content[i + 1] === "/") {
@@ -117,7 +168,7 @@ export function commentSpans(content: string, style: CommentStyle): Array<[numbe
117
168
  * exempts `pem-*` rules. Comments and normal strings are skipped so a `"""`
118
169
  * delimiter inside them is not mis-detected.
119
170
  */
120
- export function pythonDocstringSpans(content: string): Array<[number, number]> {
171
+ function pythonDocstringSpans(content: string): Array<[number, number]> {
121
172
  const spans: Array<[number, number]> = [];
122
173
  const n = content.length;
123
174
  let i = 0;
@@ -158,7 +209,7 @@ export function pythonDocstringSpans(content: string): Array<[number, number]> {
158
209
  }
159
210
 
160
211
  /** True if `offset` falls inside one of the (sorted, non-overlapping) spans. */
161
- export function offsetInSpans(spans: ReadonlyArray<[number, number]>, offset: number): boolean {
212
+ function offsetInSpans(spans: ReadonlyArray<[number, number]>, offset: number): boolean {
162
213
  let lo = 0;
163
214
  let hi = spans.length - 1;
164
215
  while (lo <= hi) {
@@ -179,7 +230,7 @@ export function offsetInSpans(spans: ReadonlyArray<[number, number]>, offset: nu
179
230
  * it is language-agnostic. `qscan-ignore-line` is not a substring of
180
231
  * `qscan-ignore-next-line`, so the two never collide.
181
232
  */
182
- export function ignoredLines(content: string): Set<number> {
233
+ function ignoredLines(content: string): Set<number> {
183
234
  const ignored = new Set<number>();
184
235
  const lines = content.split("\n");
185
236
  for (let i = 0; i < lines.length; i++) {
@@ -210,7 +261,7 @@ export function stripCommentFindings(
210
261
  if (findings.length === 0) return findings;
211
262
  const style = commentStyleForFile(file);
212
263
  if (!style) return findings;
213
- const spans = commentSpans(content, style);
264
+ const spans = commentSpans(content, style, /\.go$/i.test(file));
214
265
  // Python docstrings suppress prose *token* rules but keep real PEM material.
215
266
  const docSpans = style === "hash" ? pythonDocstringSpans(content) : [];
216
267
  if (spans.length === 0 && docSpans.length === 0) return findings;
@@ -239,7 +290,11 @@ export function stripCommentFindings(
239
290
  * suppress findings from IDENTIFIER-only rules (e.g. a Go `SigningMethodRS256`
240
291
  * mentioned inside an error-message string) — the mirror of {@link commentSpans}.
241
292
  */
242
- export function stringSpans(content: string, style: CommentStyle): Array<[number, number]> {
293
+ export function stringSpans(
294
+ content: string,
295
+ style: CommentStyle,
296
+ rawBacktick = false,
297
+ ): Array<[number, number]> {
243
298
  const spans: Array<[number, number]> = [];
244
299
  const n = content.length;
245
300
  let i = 0;
@@ -262,21 +317,11 @@ export function stringSpans(content: string, style: CommentStyle): Array<[number
262
317
  continue;
263
318
  }
264
319
  if (c === '"' || c === "'" || c === "`") {
265
- const quote = c;
266
320
  const start = i;
267
- i++;
268
- while (i < n) {
269
- if (content[i] === "\\") {
270
- i += 2;
271
- continue;
272
- }
273
- if (content[i] === quote) {
274
- i++;
275
- break;
276
- }
277
- i++;
278
- }
279
- spans.push([start, i]);
321
+ const end = skipQuoted(content, i, n, rawBacktick);
322
+ // A lone `'` (Rust lifetime, end === start+1) is not a string; skip it.
323
+ if (c !== "'" || end > start + 1) spans.push([start, end]);
324
+ i = end;
280
325
  continue;
281
326
  }
282
327
  i++;
@@ -302,7 +347,7 @@ export function stripStringLiteralFindings(
302
347
  if (findings.length === 0 || !findings.some((f) => ruleIds.has(f.ruleId))) return findings;
303
348
  const style = commentStyleForFile(file);
304
349
  if (!style) return findings;
305
- const spans = stringSpans(content, style);
350
+ const spans = stringSpans(content, style, /\.go$/i.test(file));
306
351
  if (spans.length === 0) return findings;
307
352
 
308
353
  const lineStarts: number[] = [0];
@@ -501,7 +501,8 @@ export const vulnerableDependencies: VulnerableDependency[] = [
501
501
  {
502
502
  name: "github.com/cloudflare/circl",
503
503
  ecosystem: "go",
504
- reason: "Cloudflare CIRCL — classical ECDH/EdDSA curves (X25519, X448, Ed25519, P-256).",
504
+ reason:
505
+ "Cloudflare CIRCL — classical ECDH/EdDSA curves (X25519, X448, Ed25519, P-256); also ships PQC (ML-KEM/ML-DSA + hybrids), so migrate the classical *usage*, not the package.",
505
506
  algorithms: ["ECDH", "EdDSA"],
506
507
  severity: "medium",
507
508
  },
@@ -14,7 +14,7 @@ import type {
14
14
  import { remediationText } from "./remediation.js";
15
15
 
16
16
  /** A 1-based line/column position derived from a character offset. */
17
- export interface LineCol {
17
+ interface LineCol {
18
18
  line: number;
19
19
  column: number;
20
20
  }
@@ -62,14 +62,14 @@ function lineIndexFor(starts: readonly number[], offset: number): number {
62
62
  * line/column. Newlines are LF; CR is treated as an ordinary character, so on
63
63
  * CRLF files the column includes the trailing CR offset harmlessly.
64
64
  */
65
- export function offsetToLineCol(content: string, offset: number): LineCol {
65
+ function offsetToLineCol(content: string, offset: number): LineCol {
66
66
  const starts = lineStartsFor(content);
67
67
  const idx = lineIndexFor(starts, offset);
68
68
  return { line: idx + 1, column: offset - starts[idx] + 1 };
69
69
  }
70
70
 
71
71
  /** Extract the (trimmed) single source line containing `offset`. */
72
- export function lineAt(content: string, offset: number): string {
72
+ function lineAt(content: string, offset: number): string {
73
73
  const starts = lineStartsFor(content);
74
74
  const idx = lineIndexFor(starts, offset);
75
75
  const start = starts[idx];
@@ -80,7 +80,7 @@ export function lineAt(content: string, offset: number): string {
80
80
  }
81
81
 
82
82
  /** Inputs for {@link makeFinding}. */
83
- export interface FindingSpec {
83
+ interface FindingSpec {
84
84
  ruleId: string;
85
85
  title: string;
86
86
  category: FindingCategory;
@@ -146,7 +146,7 @@ export function makeFinding(spec: FindingSpec): Finding {
146
146
  }
147
147
 
148
148
  /** Where a match occurred, plus optional per-finding field overrides. */
149
- export interface RuleMatch {
149
+ interface RuleMatch {
150
150
  file: string;
151
151
  content: string;
152
152
  /** Match start offset within `content`. */
@@ -160,7 +160,7 @@ export interface RuleMatch {
160
160
  * rules (e.g. key generation across algorithm families) override these; fixed
161
161
  * rules pass none and inherit the catalog metadata verbatim.
162
162
  */
163
- export type RuleOverrides = Partial<
163
+ type RuleOverrides = Partial<
164
164
  Pick<
165
165
  RuleMeta,
166
166
  "title" | "category" | "severity" | "confidence" | "algorithm" | "hndl" | "message" | "cwe"
@@ -223,7 +223,10 @@ export const PYTHON_EXTENSIONS: readonly string[] = [".py", ".pyi", ".pyw"];
223
223
  export const GO_EXTENSIONS: readonly string[] = [".go"];
224
224
 
225
225
  /** Java / Kotlin source extensions handled by the JCA detector. */
226
- export const JAVA_EXTENSIONS: readonly string[] = [".java", ".kt", ".kts"];
226
+ // The JVM detector also covers Kotlin (`.kt`/`.kts`) and Scala (`.scala`/`.sc`):
227
+ // all three compile against the same JCA (`KeyPairGenerator`, `Signature`,
228
+ // `KeyAgreement`) and BouncyCastle APIs the Java rules match.
229
+ export const JAVA_EXTENSIONS: readonly string[] = [".java", ".kt", ".kts", ".scala", ".sc"];
227
230
 
228
231
  /** C# source extensions handled by the .NET detector. */
229
232
  export const CSHARP_EXTENSIONS: readonly string[] = [".cs"];
@@ -234,9 +237,27 @@ export const RUST_EXTENSIONS: readonly string[] = [".rs"];
234
237
  /** Ruby source extensions handled by the Ruby detector. */
235
238
  export const RUBY_EXTENSIONS: readonly string[] = [".rb"];
236
239
 
240
+ /** Elixir source extensions handled by the Elixir detector. */
241
+ export const ELIXIR_EXTENSIONS: readonly string[] = [".ex", ".exs"];
242
+
243
+ /** PHP source extensions handled by the PHP detector. */
244
+ export const PHP_EXTENSIONS: readonly string[] = [".php", ".phtml", ".php3", ".php4", ".php5"];
245
+
237
246
  /** C / C++ source extensions handled by the OpenSSL detector. */
238
247
  export const C_EXTENSIONS: readonly string[] = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"];
239
248
 
249
+ /** Swift source extensions handled by the CryptoKit / Security-framework detector. */
250
+ export const SWIFT_EXTENSIONS: readonly string[] = [".swift"];
251
+
252
+ /** Objective-C implementation extensions (`.h` is owned by the C pack). */
253
+ export const OBJC_EXTENSIONS: readonly string[] = [".m", ".mm"];
254
+
255
+ /** Dart / Flutter source extensions. */
256
+ export const DART_EXTENSIONS: readonly string[] = [".dart"];
257
+
258
+ /** Smart-contract source extensions (Solidity/EVM, Move, Cairo/Starknet). */
259
+ export const SMART_CONTRACT_EXTENSIONS: readonly string[] = [".sol", ".move", ".cairo"];
260
+
240
261
  /**
241
262
  * Prose/documentation extensions. The language-agnostic *token* detectors (SSH
242
263
  * public keys, TLS cipher suites, certificate signature algorithms) must not run
@@ -273,6 +294,9 @@ export const JWT_HOST_EXTENSIONS: readonly string[] = [
273
294
  ...PYTHON_EXTENSIONS,
274
295
  ...GO_EXTENSIONS,
275
296
  ...RUBY_EXTENSIONS,
297
+ // PHP's firebase/php-jwt passes the alg as a quoted token (`'RS256'`, `'ES256'`),
298
+ // the same shape RE_JWT_ALG matches — so PHP files are in scope for it too.
299
+ ...PHP_EXTENSIONS,
276
300
  ];
277
301
 
278
302
  /**
@@ -291,7 +315,13 @@ export const ANALYZABLE_SOURCE_EXTENSIONS: readonly string[] = [
291
315
  ...CSHARP_EXTENSIONS,
292
316
  ...RUST_EXTENSIONS,
293
317
  ...RUBY_EXTENSIONS,
318
+ ...PHP_EXTENSIONS,
319
+ ...ELIXIR_EXTENSIONS,
294
320
  ...C_EXTENSIONS,
321
+ ...SWIFT_EXTENSIONS,
322
+ ...OBJC_EXTENSIONS,
323
+ ...DART_EXTENSIONS,
324
+ ...SMART_CONTRACT_EXTENSIONS,
295
325
  ];
296
326
 
297
327
  /**
@@ -299,7 +329,8 @@ export const ANALYZABLE_SOURCE_EXTENSIONS: readonly string[] = [
299
329
  * crypto, shown in coverage output. Kept next to {@link
300
330
  * ANALYZABLE_SOURCE_EXTENSIONS} so a new language pack updates one place.
301
331
  */
302
- export const ANALYZABLE_LANGUAGES_LABEL = "JS/TS, Python, Go, Java, C#, Rust, Ruby, C/C++";
332
+ export const ANALYZABLE_LANGUAGES_LABEL =
333
+ "JS/TS, Python, Go, Java/Kotlin/Scala, C#, Rust, Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart, Solidity/Move/Cairo";
303
334
 
304
335
  /** True when a path is in a source language the scanner can analyze for crypto. */
305
336
  export function isAnalyzableSource(filePath: string): boolean {
@@ -307,10 +338,13 @@ export function isAnalyzableSource(filePath: string): boolean {
307
338
  }
308
339
 
309
340
  /**
310
- * Given a SORTED ascending array of call offsets, return true when `idx` is at
311
- * or after some call offset `c` with `idx - c < window`. Runs in O(log n) by
312
- * binary-searching the largest call offset `idx` and checking the gap. This
313
- * replaces the previous O(matches × calls) linear scan (`nearCall`).
341
+ * Given a SORTED ascending array of call offsets, return true when `idx` is at or
342
+ * after some call offset `c` with `idx - c < window`. Deliberately ONE-SIDED
343
+ * (forward only): the WebCrypto algorithm name follows the `subtle.*(` call it
344
+ * belongs to, and reaching backward would wrongly attribute an UNRELATED earlier
345
+ * call's argument (e.g. a Node `generateKeyPairSync('rsa-pss', …)` sitting a few
346
+ * lines above a subtle call) to the wrong API. Runs in O(log n) by binary-searching
347
+ * the largest call offset ≤ `idx` and checking the gap.
314
348
  */
315
349
  export function nearSortedCall(
316
350
  sortedCalls: readonly number[],
@@ -351,3 +385,103 @@ export function eachMatch(
351
385
  if (m.index === g.lastIndex) g.lastIndex++; // avoid infinite loop on empty match
352
386
  }
353
387
  }
388
+
389
+ /**
390
+ * Return the innermost `{ … }` object that contains `index`, as a substring — so a
391
+ * per-match check (e.g. a JWK's own `use`/`alg`, or whether an `alg` sits inside a
392
+ * JWK) analyses ONLY that object and can't be contaminated by a neighbouring object
393
+ * in a packed array. The scan is STRING-AWARE (braces and quotes inside JSON string
394
+ * values, honouring `\"`, are ignored), so a value like `"a}b{c"` can't mis-scope the
395
+ * object. Bounded by `maxSpan` each way so a pathological input can't blow up; falls
396
+ * back to a bounded ±window when no enclosing object is found in range.
397
+ */
398
+ export function enclosingObject(content: string, index: number, maxSpan = 4000): string {
399
+ const lo = Math.max(0, index - maxSpan);
400
+ const hi = Math.min(content.length, index + maxSpan);
401
+ const stack: number[] = []; // positions of currently-open `{`
402
+ let inStr = false;
403
+ let esc = false;
404
+ let enclosingOpen = -1; // innermost `{` open at `index`
405
+ for (let i = lo; i < hi; i++) {
406
+ const c = content[i];
407
+ if (inStr) {
408
+ if (esc) esc = false;
409
+ else if (c === "\\") esc = true;
410
+ else if (c === '"') inStr = false;
411
+ } else if (c === '"') {
412
+ inStr = true;
413
+ } else if (c === "{") {
414
+ stack.push(i);
415
+ } else if (c === "}") {
416
+ const open = stack.pop();
417
+ if (open !== undefined && open === enclosingOpen) {
418
+ return content.slice(enclosingOpen, i + 1); // closed the enclosing object
419
+ }
420
+ }
421
+ if (i === index && enclosingOpen < 0 && stack.length > 0) {
422
+ enclosingOpen = stack[stack.length - 1];
423
+ }
424
+ }
425
+ // No complete enclosing object in range: if we found an (unclosed) enclosing open
426
+ // brace, return from it to hi. Otherwise the token is not inside any object (top-level
427
+ // token / brace-less config like YAML) — return a bounded ±250 window, NOT the full
428
+ // forward span, so a `"kty"` far AFTER the token can't pull an unrelated JWK into
429
+ // scope and over-suppress a legitimate standalone finding.
430
+ if (enclosingOpen >= 0) return content.slice(enclosingOpen, hi);
431
+ return content.slice(Math.max(0, index - 250), Math.min(content.length, index + 250));
432
+ }
433
+
434
+ /**
435
+ * Blank out FULL-LINE comments so a commented-out directive isn't reported as an
436
+ * active setting. A line counts as a comment when its first non-whitespace characters
437
+ * match one of `markers` (e.g. `#`, `//`, `;`, `!`). Each such line's characters are
438
+ * replaced with spaces of the SAME length (newlines preserved), so every byte offset
439
+ * is unchanged — the finding line/column/snippet for the non-comment lines that remain
440
+ * stay exactly correct. Inline trailing comments are intentionally left alone (the
441
+ * directive before them is still active, so it should still be flagged).
442
+ *
443
+ * Config detectors run their rules over the masked content; matches can then only land
444
+ * on live config, not on commented examples (mosquitto.conf, CI YAML, ipsec.conf, and
445
+ * HCL all conventionally ship large blocks of commented-out directives).
446
+ */
447
+ export function maskCommentLines(content: string, markers: readonly string[]): string {
448
+ if (markers.length === 0) return content;
449
+ const lines = content.split("\n");
450
+ for (let i = 0; i < lines.length; i++) {
451
+ const line = lines[i];
452
+ const lead = line.trimStart();
453
+ if (lead !== "" && markers.some((mk) => lead.startsWith(mk))) {
454
+ lines[i] = " ".repeat(line.length);
455
+ }
456
+ }
457
+ return lines.join("\n");
458
+ }
459
+
460
+ /**
461
+ * Blank out C-style block comments (the `slash-star … star-slash` form, multi-line
462
+ * included), replacing every
463
+ * character with a space and preserving newlines so byte offsets — and therefore
464
+ * finding line/column — stay exact. Complements {@link maskCommentLines}, which only
465
+ * masks whole single-line comments: an HCL/Bicep resource wrapped in a block comment
466
+ * has its inner lines mid-block (they don't START with `/*`), so a line-only masker
467
+ * leaves them live. Not string-aware — a literal `/*` inside a config string value is
468
+ * rare and, for these token detectors, a tolerable over-mask.
469
+ */
470
+ export function maskBlockComments(content: string): string {
471
+ if (!content.includes("/*")) return content;
472
+ const out: string[] = [];
473
+ let i = 0;
474
+ const n = content.length;
475
+ while (i < n) {
476
+ if (content[i] === "/" && content[i + 1] === "*") {
477
+ const end = content.indexOf("*/", i + 2);
478
+ const stop = end === -1 ? n : end + 2;
479
+ for (let j = i; j < stop; j++) out.push(content[j] === "\n" ? "\n" : " ");
480
+ i = stop;
481
+ } else {
482
+ out.push(content[i]);
483
+ i++;
484
+ }
485
+ }
486
+ return out.join("");
487
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Config detector: age IDENTITY (private) keys committed to disk. The secrets
3
+ * detector flags age *recipients* (`age1…` public keys); this flags the far worse
4
+ * case — an `AGE-SECRET-KEY-1…` private identity checked into a repo or config.
5
+ * age identities are X25519 secret keys: a classical key-agreement secret that,
6
+ * once leaked or committed, lets an attacker decrypt every age-wrapped payload
7
+ * addressed to it (harvest-now-decrypt-later, and retroactively un-fixable if the
8
+ * ciphertext is already in git history).
9
+ *
10
+ * The matched value IS the private key, so it is marked sensitive and reporters
11
+ * drop the snippet.
12
+ */
13
+ import type { Detector, Finding, RuleMeta } from "../types.js";
14
+ import { eachMatch, findingFromRule } from "../detect-utils.js";
15
+ import { CWE_HARDCODED_KEY } from "../cwe.js";
16
+
17
+ // age secret keys are Bech32 in uppercase: `AGE-SECRET-KEY-1` + data part.
18
+ const RE_AGE_SECRET = /\bAGE-SECRET-KEY-1[0-9A-Z]{50,}\b/g;
19
+
20
+ const RULE_AGE_SECRET: RuleMeta = {
21
+ id: "age-secret-key",
22
+ title: "age identity (X25519 private key)",
23
+ description: "An age AGE-SECRET-KEY-1 private identity key committed to disk",
24
+ category: "key-exchange",
25
+ severity: "high",
26
+ confidence: "high",
27
+ algorithm: "X25519",
28
+ hndl: true,
29
+ cwe: CWE_HARDCODED_KEY,
30
+ sensitive: true,
31
+ message:
32
+ "An age identity (X25519 private key) is committed to disk; every age-wrapped payload addressed to it is decryptable by anyone with this key, and the exposure is retroactive if the ciphertext is already in git history.",
33
+ remediation:
34
+ "Rotate the age identity and re-encrypt affected data; move the private key to a secret manager. Plan for a post-quantum KEM (ML-KEM) recipient when available.",
35
+ };
36
+
37
+ /** Detects committed age identity (private) keys. */
38
+ export const ageDetector: Detector = {
39
+ id: "age-identity",
40
+ description: "Committed age identity (X25519 private) keys",
41
+ scope: "config",
42
+ language: "any",
43
+ rules: [RULE_AGE_SECRET],
44
+ appliesTo: () => true,
45
+ detect({ file, content }): Finding[] {
46
+ if (!content.includes("AGE-SECRET-KEY-1")) return [];
47
+ const findings: Finding[] = [];
48
+ eachMatch(RE_AGE_SECRET, content, (m) =>
49
+ findings.push(
50
+ findingFromRule(RULE_AGE_SECRET, {
51
+ file,
52
+ content,
53
+ index: m.index,
54
+ matchLength: m[0].length,
55
+ }),
56
+ ),
57
+ );
58
+ return findings;
59
+ },
60
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Config detector: classical asymmetric keys provisioned by Ansible's
3
+ * `community.crypto` collection. Playbooks that mint TLS/SSH key material with
4
+ * `openssl_privatekey` / `openssl_csr` / `x509_certificate` provision real
5
+ * classical keys the language packs never see — the IaC surface, in YAML.
6
+ *
7
+ * Gated to a `community.crypto` / `openssl_privatekey` marker so a bare
8
+ * `type: RSA` in unrelated YAML does not fire.
9
+ */
10
+ import type { Detector, Finding, RuleMeta } from "../types.js";
11
+ import { eachMatch, findingFromRule, hasExtension, maskCommentLines } from "../detect-utils.js";
12
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
13
+
14
+ const YAML_EXTENSIONS: readonly string[] = [".yml", ".yaml"];
15
+
16
+ const RE_ANSIBLE_RSA = /\btype:\s*["']?RSA\b/g;
17
+ const RE_ANSIBLE_ECC = /\btype:\s*["']?ECC\b/g;
18
+ // community.crypto also supports these `type:` values, all classical.
19
+ const RE_ANSIBLE_EDDSA = /\btype:\s*["']?Ed(?:25519|448)\b/g;
20
+ const RE_ANSIBLE_XDH = /\btype:\s*["']?X(?:25519|448)\b/g;
21
+ const RE_ANSIBLE_DSA = /\btype:\s*["']?DSA\b/g;
22
+
23
+ const RULE_ANSIBLE_RSA: RuleMeta = {
24
+ id: "ansible-openssl-rsa",
25
+ title: "Ansible community.crypto RSA key",
26
+ description: "Ansible community.crypto openssl_privatekey/csr with type: RSA",
27
+ category: "kem",
28
+ severity: "medium",
29
+ confidence: "high",
30
+ algorithm: "RSA",
31
+ hndl: true,
32
+ cwe: CWE_BROKEN_CRYPTO,
33
+ message: "Ansible provisions a classical RSA key (community.crypto), which is not quantum-safe.",
34
+ remediation:
35
+ "Plan migration to PQC keys (ML-KEM-768 / ML-DSA-65) as the collection adds support.",
36
+ };
37
+ const RULE_ANSIBLE_ECC: RuleMeta = {
38
+ id: "ansible-openssl-ecc",
39
+ title: "Ansible community.crypto EC key",
40
+ description: "Ansible community.crypto openssl_privatekey/csr with type: ECC",
41
+ category: "key-exchange",
42
+ severity: "medium",
43
+ confidence: "high",
44
+ algorithm: "ECDH",
45
+ hndl: true,
46
+ cwe: CWE_BROKEN_CRYPTO,
47
+ message:
48
+ "Ansible provisions a classical EC key (community.crypto); EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
49
+ remediation:
50
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
51
+ };
52
+
53
+ const RULE_ANSIBLE_EDDSA: RuleMeta = {
54
+ id: "ansible-openssl-eddsa",
55
+ title: "Ansible community.crypto Ed25519/Ed448 key",
56
+ description: "Ansible community.crypto openssl_privatekey with type: Ed25519/Ed448",
57
+ category: "signature",
58
+ severity: "medium",
59
+ confidence: "high",
60
+ algorithm: "EdDSA",
61
+ hndl: false,
62
+ cwe: CWE_BROKEN_CRYPTO,
63
+ message:
64
+ "Ansible provisions a classical Ed25519/Ed448 key (community.crypto), forgeable by a quantum attacker.",
65
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
66
+ };
67
+ const RULE_ANSIBLE_XDH: RuleMeta = {
68
+ id: "ansible-openssl-xdh",
69
+ title: "Ansible community.crypto X25519/X448 key",
70
+ description: "Ansible community.crypto openssl_privatekey with type: X25519/X448",
71
+ category: "key-exchange",
72
+ severity: "medium",
73
+ confidence: "high",
74
+ algorithm: "X25519",
75
+ hndl: true,
76
+ cwe: CWE_BROKEN_CRYPTO,
77
+ message:
78
+ "Ansible provisions a classical X25519/X448 key-agreement key (community.crypto), which is harvest-now-decrypt-later exposed.",
79
+ remediation: "Migrate key agreement to hybrid X25519MLKEM768 (ML-KEM-768).",
80
+ };
81
+ const RULE_ANSIBLE_DSA: RuleMeta = {
82
+ id: "ansible-openssl-dsa",
83
+ title: "Ansible community.crypto DSA key (deprecated)",
84
+ description: "Ansible community.crypto openssl_privatekey with type: DSA",
85
+ category: "signature",
86
+ severity: "medium",
87
+ confidence: "high",
88
+ algorithm: "DSA",
89
+ hndl: false,
90
+ cwe: CWE_BROKEN_CRYPTO,
91
+ message:
92
+ "Ansible provisions a classical DSA key (community.crypto); deprecated and forgeable by a quantum attacker.",
93
+ remediation: "Rotate off DSA today; migrate to ML-DSA-65 (FIPS 204).",
94
+ };
95
+
96
+ /** Detects classical asymmetric keys provisioned by Ansible community.crypto. */
97
+ export const ansibleDetector: Detector = {
98
+ id: "ansible-crypto",
99
+ description: "Classical asymmetric keys provisioned by Ansible community.crypto",
100
+ scope: "config",
101
+ language: "any",
102
+ rules: [
103
+ RULE_ANSIBLE_RSA,
104
+ RULE_ANSIBLE_ECC,
105
+ RULE_ANSIBLE_EDDSA,
106
+ RULE_ANSIBLE_XDH,
107
+ RULE_ANSIBLE_DSA,
108
+ ],
109
+ appliesTo: (f) => hasExtension(f, YAML_EXTENSIONS),
110
+ detect({ file, content }): Finding[] {
111
+ // Gate: only inside a community.crypto / openssl_privatekey context.
112
+ if (!content.includes("community.crypto") && !content.includes("openssl_privatekey")) return [];
113
+ // YAML `#` comments are not active settings; match over comment-masked content
114
+ // (offsets preserved) so a `# type: RSA` note does not fire.
115
+ const scan = maskCommentLines(content, ["#"]);
116
+ const findings: Finding[] = [];
117
+ const add = (re: RegExp, rule: RuleMeta) =>
118
+ eachMatch(re, scan, (m) =>
119
+ findings.push(
120
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
121
+ ),
122
+ );
123
+ add(RE_ANSIBLE_RSA, RULE_ANSIBLE_RSA);
124
+ add(RE_ANSIBLE_ECC, RULE_ANSIBLE_ECC);
125
+ add(RE_ANSIBLE_EDDSA, RULE_ANSIBLE_EDDSA);
126
+ // Report the matched curve's own algorithm (X448 vs X25519) instead of the rule's
127
+ // default label.
128
+ eachMatch(RE_ANSIBLE_XDH, scan, (m) =>
129
+ findings.push(
130
+ findingFromRule(
131
+ RULE_ANSIBLE_XDH,
132
+ { file, content, index: m.index, matchLength: m[0].length },
133
+ m[0].includes("448") ? { algorithm: "X448" } : undefined,
134
+ ),
135
+ ),
136
+ );
137
+ add(RE_ANSIBLE_DSA, RULE_ANSIBLE_DSA);
138
+ return findings;
139
+ },
140
+ };