@quantakrypto/core 0.4.4 → 0.6.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 (302) 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/crypto-agility.d.ts +158 -0
  14. package/dist/crypto-agility.d.ts.map +1 -0
  15. package/dist/crypto-agility.js +285 -0
  16. package/dist/crypto-agility.js.map +1 -0
  17. package/dist/dependencies.d.ts.map +1 -1
  18. package/dist/dependencies.js +180 -18
  19. package/dist/dependencies.js.map +1 -1
  20. package/dist/detect-utils.d.ts +55 -21
  21. package/dist/detect-utils.d.ts.map +1 -1
  22. package/dist/detect-utils.js +133 -7
  23. package/dist/detect-utils.js.map +1 -1
  24. package/dist/detectors/age.d.ts +16 -0
  25. package/dist/detectors/age.d.ts.map +1 -0
  26. package/dist/detectors/age.js +40 -0
  27. package/dist/detectors/age.js.map +1 -0
  28. package/dist/detectors/ansible.d.ts +13 -0
  29. package/dist/detectors/ansible.d.ts.map +1 -0
  30. package/dist/detectors/ansible.js +108 -0
  31. package/dist/detectors/ansible.js.map +1 -0
  32. package/dist/detectors/bicep.d.ts +20 -0
  33. package/dist/detectors/bicep.d.ts.map +1 -0
  34. package/dist/detectors/bicep.js +74 -0
  35. package/dist/detectors/bicep.js.map +1 -0
  36. package/dist/detectors/c.d.ts.map +1 -1
  37. package/dist/detectors/c.js +30 -3
  38. package/dist/detectors/c.js.map +1 -1
  39. package/dist/detectors/cicd.d.ts.map +1 -1
  40. package/dist/detectors/cicd.js +18 -5
  41. package/dist/detectors/cicd.js.map +1 -1
  42. package/dist/detectors/cloud-kms.d.ts +8 -5
  43. package/dist/detectors/cloud-kms.d.ts.map +1 -1
  44. package/dist/detectors/cloud-kms.js +62 -20
  45. package/dist/detectors/cloud-kms.js.map +1 -1
  46. package/dist/detectors/cloudformation.d.ts +8 -0
  47. package/dist/detectors/cloudformation.d.ts.map +1 -1
  48. package/dist/detectors/cloudformation.js +41 -10
  49. package/dist/detectors/cloudformation.js.map +1 -1
  50. package/dist/detectors/codesign.d.ts +50 -0
  51. package/dist/detectors/codesign.d.ts.map +1 -0
  52. package/dist/detectors/codesign.js +143 -0
  53. package/dist/detectors/codesign.js.map +1 -0
  54. package/dist/detectors/csharp.d.ts.map +1 -1
  55. package/dist/detectors/csharp.js +13 -3
  56. package/dist/detectors/csharp.js.map +1 -1
  57. package/dist/detectors/dart.d.ts +35 -0
  58. package/dist/detectors/dart.d.ts.map +1 -0
  59. package/dist/detectors/dart.js +177 -0
  60. package/dist/detectors/dart.js.map +1 -0
  61. package/dist/detectors/database.d.ts +5 -3
  62. package/dist/detectors/database.d.ts.map +1 -1
  63. package/dist/detectors/database.js +45 -8
  64. package/dist/detectors/database.js.map +1 -1
  65. package/dist/detectors/dkim.d.ts +54 -0
  66. package/dist/detectors/dkim.d.ts.map +1 -0
  67. package/dist/detectors/dkim.js +92 -0
  68. package/dist/detectors/dkim.js.map +1 -0
  69. package/dist/detectors/dnssec.d.ts.map +1 -1
  70. package/dist/detectors/dnssec.js +19 -10
  71. package/dist/detectors/dnssec.js.map +1 -1
  72. package/dist/detectors/elixir.d.ts.map +1 -1
  73. package/dist/detectors/elixir.js +34 -2
  74. package/dist/detectors/elixir.js.map +1 -1
  75. package/dist/detectors/java.d.ts.map +1 -1
  76. package/dist/detectors/java.js +33 -3
  77. package/dist/detectors/java.js.map +1 -1
  78. package/dist/detectors/jose.d.ts.map +1 -1
  79. package/dist/detectors/jose.js +13 -3
  80. package/dist/detectors/jose.js.map +1 -1
  81. package/dist/detectors/jwk.d.ts +7 -7
  82. package/dist/detectors/jwk.d.ts.map +1 -1
  83. package/dist/detectors/jwk.js +44 -8
  84. package/dist/detectors/jwk.js.map +1 -1
  85. package/dist/detectors/k8s.d.ts.map +1 -1
  86. package/dist/detectors/k8s.js +5 -2
  87. package/dist/detectors/k8s.js.map +1 -1
  88. package/dist/detectors/keystore.d.ts +19 -0
  89. package/dist/detectors/keystore.d.ts.map +1 -0
  90. package/dist/detectors/keystore.js +76 -0
  91. package/dist/detectors/keystore.js.map +1 -0
  92. package/dist/detectors/mesh.d.ts +7 -8
  93. package/dist/detectors/mesh.d.ts.map +1 -1
  94. package/dist/detectors/mesh.js +17 -35
  95. package/dist/detectors/mesh.js.map +1 -1
  96. package/dist/detectors/messaging.d.ts +2 -2
  97. package/dist/detectors/messaging.d.ts.map +1 -1
  98. package/dist/detectors/messaging.js +19 -12
  99. package/dist/detectors/messaging.js.map +1 -1
  100. package/dist/detectors/objc.d.ts +41 -0
  101. package/dist/detectors/objc.d.ts.map +1 -0
  102. package/dist/detectors/objc.js +146 -0
  103. package/dist/detectors/objc.js.map +1 -0
  104. package/dist/detectors/openpgp.d.ts +20 -0
  105. package/dist/detectors/openpgp.d.ts.map +1 -0
  106. package/dist/detectors/openpgp.js +151 -0
  107. package/dist/detectors/openpgp.js.map +1 -0
  108. package/dist/detectors/pem.d.ts.map +1 -1
  109. package/dist/detectors/pem.js +32 -0
  110. package/dist/detectors/pem.js.map +1 -1
  111. package/dist/detectors/php.d.ts +2 -1
  112. package/dist/detectors/php.d.ts.map +1 -1
  113. package/dist/detectors/php.js +7 -3
  114. package/dist/detectors/php.js.map +1 -1
  115. package/dist/detectors/pkcs11.d.ts +17 -0
  116. package/dist/detectors/pkcs11.d.ts.map +1 -0
  117. package/dist/detectors/pkcs11.js +87 -0
  118. package/dist/detectors/pkcs11.js.map +1 -0
  119. package/dist/detectors/proxy.d.ts +52 -0
  120. package/dist/detectors/proxy.d.ts.map +1 -0
  121. package/dist/detectors/proxy.js +157 -0
  122. package/dist/detectors/proxy.js.map +1 -0
  123. package/dist/detectors/pulumi.d.ts +19 -0
  124. package/dist/detectors/pulumi.d.ts.map +1 -0
  125. package/dist/detectors/pulumi.js +80 -0
  126. package/dist/detectors/pulumi.js.map +1 -0
  127. package/dist/detectors/rust.d.ts.map +1 -1
  128. package/dist/detectors/rust.js +5 -0
  129. package/dist/detectors/rust.js.map +1 -1
  130. package/dist/detectors/secrets.d.ts +4 -1
  131. package/dist/detectors/secrets.d.ts.map +1 -1
  132. package/dist/detectors/secrets.js +5 -21
  133. package/dist/detectors/secrets.js.map +1 -1
  134. package/dist/detectors/solidity.d.ts +50 -0
  135. package/dist/detectors/solidity.d.ts.map +1 -0
  136. package/dist/detectors/solidity.js +108 -0
  137. package/dist/detectors/solidity.js.map +1 -0
  138. package/dist/detectors/source.d.ts.map +1 -1
  139. package/dist/detectors/source.js +105 -6
  140. package/dist/detectors/source.js.map +1 -1
  141. package/dist/detectors/spire.d.ts +49 -0
  142. package/dist/detectors/spire.d.ts.map +1 -0
  143. package/dist/detectors/spire.js +77 -0
  144. package/dist/detectors/spire.js.map +1 -0
  145. package/dist/detectors/ssh-ca.d.ts +72 -0
  146. package/dist/detectors/ssh-ca.d.ts.map +1 -0
  147. package/dist/detectors/ssh-ca.js +119 -0
  148. package/dist/detectors/ssh-ca.js.map +1 -0
  149. package/dist/detectors/supply-chain.d.ts +17 -0
  150. package/dist/detectors/supply-chain.d.ts.map +1 -0
  151. package/dist/detectors/supply-chain.js +89 -0
  152. package/dist/detectors/supply-chain.js.map +1 -0
  153. package/dist/detectors/swift.d.ts +22 -0
  154. package/dist/detectors/swift.d.ts.map +1 -0
  155. package/dist/detectors/swift.js +120 -0
  156. package/dist/detectors/swift.js.map +1 -0
  157. package/dist/detectors/terraform.d.ts.map +1 -1
  158. package/dist/detectors/terraform.js +35 -8
  159. package/dist/detectors/terraform.js.map +1 -1
  160. package/dist/detectors/vault.d.ts +15 -0
  161. package/dist/detectors/vault.d.ts.map +1 -0
  162. package/dist/detectors/vault.js +97 -0
  163. package/dist/detectors/vault.js.map +1 -0
  164. package/dist/detectors/vpn.d.ts +8 -8
  165. package/dist/detectors/vpn.d.ts.map +1 -1
  166. package/dist/detectors/vpn.js +12 -38
  167. package/dist/detectors/vpn.js.map +1 -1
  168. package/dist/detectors/weak-hash.d.ts +71 -0
  169. package/dist/detectors/weak-hash.d.ts.map +1 -0
  170. package/dist/detectors/weak-hash.js +116 -0
  171. package/dist/detectors/weak-hash.js.map +1 -0
  172. package/dist/detectors/webauthn.d.ts +61 -0
  173. package/dist/detectors/webauthn.d.ts.map +1 -0
  174. package/dist/detectors/webauthn.js +109 -0
  175. package/dist/detectors/webauthn.js.map +1 -0
  176. package/dist/detectors/xmldsig.d.ts +23 -0
  177. package/dist/detectors/xmldsig.d.ts.map +1 -0
  178. package/dist/detectors/xmldsig.js +84 -0
  179. package/dist/detectors/xmldsig.js.map +1 -0
  180. package/dist/evidence.d.ts +74 -4
  181. package/dist/evidence.d.ts.map +1 -1
  182. package/dist/evidence.js +71 -0
  183. package/dist/evidence.js.map +1 -1
  184. package/dist/hndl.d.ts +241 -0
  185. package/dist/hndl.d.ts.map +1 -0
  186. package/dist/hndl.js +752 -0
  187. package/dist/hndl.js.map +1 -0
  188. package/dist/index.d.ts +18 -11
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +21 -11
  191. package/dist/index.js.map +1 -1
  192. package/dist/inventory.d.ts +0 -2
  193. package/dist/inventory.d.ts.map +1 -1
  194. package/dist/inventory.js +15 -6
  195. package/dist/inventory.js.map +1 -1
  196. package/dist/parallel.d.ts.map +1 -1
  197. package/dist/parallel.js +22 -23
  198. package/dist/parallel.js.map +1 -1
  199. package/dist/registry.d.ts +9 -4
  200. package/dist/registry.d.ts.map +1 -1
  201. package/dist/registry.js +51 -4
  202. package/dist/registry.js.map +1 -1
  203. package/dist/remediate-pipeline.d.ts +0 -3
  204. package/dist/remediate-pipeline.d.ts.map +1 -1
  205. package/dist/remediate-pipeline.js +1 -1
  206. package/dist/remediate-pipeline.js.map +1 -1
  207. package/dist/remediation.d.ts +10 -0
  208. package/dist/remediation.d.ts.map +1 -1
  209. package/dist/remediation.js +32 -0
  210. package/dist/remediation.js.map +1 -1
  211. package/dist/report.d.ts +17 -0
  212. package/dist/report.d.ts.map +1 -1
  213. package/dist/report.js +106 -20
  214. package/dist/report.js.map +1 -1
  215. package/dist/scan-worker.js +5 -3
  216. package/dist/scan-worker.js.map +1 -1
  217. package/dist/scan.d.ts.map +1 -1
  218. package/dist/scan.js +35 -6
  219. package/dist/scan.js.map +1 -1
  220. package/dist/standards-profiles.d.ts +58 -0
  221. package/dist/standards-profiles.d.ts.map +1 -0
  222. package/dist/standards-profiles.js +100 -0
  223. package/dist/standards-profiles.js.map +1 -0
  224. package/dist/types.d.ts +7 -6
  225. package/dist/types.d.ts.map +1 -1
  226. package/dist/types.js.map +1 -1
  227. package/dist/version.d.ts +1 -1
  228. package/dist/version.js +1 -1
  229. package/dist/version.js.map +1 -1
  230. package/dist/vex.d.ts +39 -0
  231. package/dist/vex.d.ts.map +1 -0
  232. package/dist/vex.js +87 -0
  233. package/dist/vex.js.map +1 -0
  234. package/dist/walk.d.ts +39 -4
  235. package/dist/walk.d.ts.map +1 -1
  236. package/dist/walk.js +62 -10
  237. package/dist/walk.js.map +1 -1
  238. package/package.json +1 -1
  239. package/src/cbom-merge.ts +38 -8
  240. package/src/cbom.ts +153 -39
  241. package/src/comments.ts +25 -6
  242. package/src/crypto-agility.ts +407 -0
  243. package/src/dependencies.ts +182 -16
  244. package/src/detect-utils.ts +133 -11
  245. package/src/detectors/age.ts +60 -0
  246. package/src/detectors/ansible.ts +140 -0
  247. package/src/detectors/bicep.ts +107 -0
  248. package/src/detectors/c.ts +31 -3
  249. package/src/detectors/cicd.ts +18 -5
  250. package/src/detectors/cloud-kms.ts +79 -25
  251. package/src/detectors/cloudformation.ts +45 -10
  252. package/src/detectors/codesign.ts +214 -0
  253. package/src/detectors/csharp.ts +13 -3
  254. package/src/detectors/dart.ts +237 -0
  255. package/src/detectors/database.ts +64 -11
  256. package/src/detectors/dkim.ts +172 -0
  257. package/src/detectors/dnssec.ts +19 -10
  258. package/src/detectors/elixir.ts +33 -2
  259. package/src/detectors/java.ts +37 -3
  260. package/src/detectors/jose.ts +19 -3
  261. package/src/detectors/jwk.ts +59 -17
  262. package/src/detectors/k8s.ts +5 -2
  263. package/src/detectors/keystore.ts +109 -0
  264. package/src/detectors/mesh.ts +21 -45
  265. package/src/detectors/messaging.ts +22 -14
  266. package/src/detectors/objc.ts +209 -0
  267. package/src/detectors/openpgp.ts +194 -0
  268. package/src/detectors/pem.ts +30 -0
  269. package/src/detectors/php.ts +9 -4
  270. package/src/detectors/pkcs11.ts +113 -0
  271. package/src/detectors/proxy.ts +236 -0
  272. package/src/detectors/pulumi.ts +106 -0
  273. package/src/detectors/rust.ts +5 -0
  274. package/src/detectors/secrets.ts +15 -26
  275. package/src/detectors/solidity.ts +176 -0
  276. package/src/detectors/source.ts +110 -5
  277. package/src/detectors/spire.ts +143 -0
  278. package/src/detectors/ssh-ca.ts +215 -0
  279. package/src/detectors/supply-chain.ts +117 -0
  280. package/src/detectors/swift.ts +149 -0
  281. package/src/detectors/terraform.ts +42 -8
  282. package/src/detectors/vault.ts +120 -0
  283. package/src/detectors/vpn.ts +20 -48
  284. package/src/detectors/weak-hash.ts +217 -0
  285. package/src/detectors/webauthn.ts +205 -0
  286. package/src/detectors/xmldsig.ts +115 -0
  287. package/src/evidence.ts +127 -4
  288. package/src/hndl.ts +1012 -0
  289. package/src/index.ts +93 -13
  290. package/src/inventory.ts +15 -7
  291. package/src/parallel.ts +21 -20
  292. package/src/registry.ts +51 -4
  293. package/src/remediate-pipeline.ts +1 -1
  294. package/src/remediation.ts +39 -0
  295. package/src/report.ts +120 -20
  296. package/src/scan-worker.ts +5 -3
  297. package/src/scan.ts +41 -6
  298. package/src/standards-profiles.ts +136 -0
  299. package/src/types.ts +18 -5
  300. package/src/version.ts +1 -1
  301. package/src/vex.ts +122 -0
  302. package/src/walk.ts +80 -12
@@ -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"
@@ -246,6 +246,18 @@ export const PHP_EXTENSIONS: readonly string[] = [".php", ".phtml", ".php3", ".p
246
246
  /** C / C++ source extensions handled by the OpenSSL detector. */
247
247
  export const C_EXTENSIONS: readonly string[] = [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hh"];
248
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
+
249
261
  /**
250
262
  * Prose/documentation extensions. The language-agnostic *token* detectors (SSH
251
263
  * public keys, TLS cipher suites, certificate signature algorithms) must not run
@@ -282,6 +294,9 @@ export const JWT_HOST_EXTENSIONS: readonly string[] = [
282
294
  ...PYTHON_EXTENSIONS,
283
295
  ...GO_EXTENSIONS,
284
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,
285
300
  ];
286
301
 
287
302
  /**
@@ -303,6 +318,10 @@ export const ANALYZABLE_SOURCE_EXTENSIONS: readonly string[] = [
303
318
  ...PHP_EXTENSIONS,
304
319
  ...ELIXIR_EXTENSIONS,
305
320
  ...C_EXTENSIONS,
321
+ ...SWIFT_EXTENSIONS,
322
+ ...OBJC_EXTENSIONS,
323
+ ...DART_EXTENSIONS,
324
+ ...SMART_CONTRACT_EXTENSIONS,
306
325
  ];
307
326
 
308
327
  /**
@@ -311,7 +330,7 @@ export const ANALYZABLE_SOURCE_EXTENSIONS: readonly string[] = [
311
330
  * ANALYZABLE_SOURCE_EXTENSIONS} so a new language pack updates one place.
312
331
  */
313
332
  export const ANALYZABLE_LANGUAGES_LABEL =
314
- "JS/TS, Python, Go, Java/Kotlin/Scala, C#, Rust, Ruby, PHP, Elixir, C/C++";
333
+ "JS/TS, Python, Go, Java/Kotlin/Scala, C#, Rust, Ruby, PHP, Elixir, C/C++, Swift, Objective-C, Dart, Solidity/Move/Cairo";
315
334
 
316
335
  /** True when a path is in a source language the scanner can analyze for crypto. */
317
336
  export function isAnalyzableSource(filePath: string): boolean {
@@ -319,10 +338,13 @@ export function isAnalyzableSource(filePath: string): boolean {
319
338
  }
320
339
 
321
340
  /**
322
- * Given a SORTED ascending array of call offsets, return true when `idx` is at
323
- * or after some call offset `c` with `idx - c < window`. Runs in O(log n) by
324
- * binary-searching the largest call offset `idx` and checking the gap. This
325
- * 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.
326
348
  */
327
349
  export function nearSortedCall(
328
350
  sortedCalls: readonly number[],
@@ -363,3 +385,103 @@ export function eachMatch(
363
385
  if (m.index === g.lastIndex) g.lastIndex++; // avoid infinite loop on empty match
364
386
  }
365
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
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * IaC detector: classical asymmetric cryptography and legacy TLS declared in Azure
3
+ * Bicep (`.bicep`) — Azure's native IaC DSL, distinct from the ARM/CloudFormation
4
+ * JSON the cloudformation detector already covers (Bicep is the *source*; ARM JSON is
5
+ * its compiled output). Bicep provisions the same Key Vault keys and TLS floors, but
6
+ * in its own single-quoted-string syntax that the JSON detector never sees.
7
+ *
8
+ * Covered (gated to `.bicep` files):
9
+ * - `Microsoft.KeyVault/vaults/keys` `kty: 'RSA' | 'RSA-HSM' | 'EC' | 'EC-HSM'` — a
10
+ * classical Key Vault key. RSA is KEM (HNDL); EC feeds ECDSA + ECDH (the ECDH path
11
+ * is HNDL). Gated to a `Microsoft.KeyVault` marker so a stray `kty` elsewhere in a
12
+ * template does not fire.
13
+ * - `minimumTlsVersion: 'TLS1_0' | 'TLS1_1'` — a deprecated TLS floor on storage /
14
+ * app-service / other resources (RFC 8996). Not HNDL (a config weakness), category
15
+ * "tls".
16
+ */
17
+ import type { Detector, Finding, RuleMeta } from "../types.js";
18
+ import {
19
+ eachMatch,
20
+ findingFromRule,
21
+ hasExtension,
22
+ maskBlockComments,
23
+ maskCommentLines,
24
+ } from "../detect-utils.js";
25
+ import { CWE_BROKEN_CRYPTO, CWE_WEAK_STRENGTH } from "../cwe.js";
26
+
27
+ const BICEP_EXTENSIONS: readonly string[] = [".bicep"];
28
+
29
+ // Bicep string values are single-quoted. `kty: 'RSA'` / `'RSA-HSM'` (KEM) and
30
+ // `kty: 'EC'` / `'EC-HSM'` (EC). The `(?<![\w-])` lookbehind stops a longer key name
31
+ // from matching on a `kty` suffix; the value bound keeps `'EC'` from matching `'ECX'`.
32
+ const RE_BICEP_KTY_RSA = /(?<![\w-])kty\s*:\s*'RSA(?:-HSM)?'/g;
33
+ const RE_BICEP_KTY_EC = /(?<![\w-])kty\s*:\s*'EC(?:-HSM)?'/g;
34
+ // Legacy TLS floor. `TLS1_0` / `TLS1_1` are deprecated; `TLS1_2` / `TLS1_3` are fine,
35
+ // and the closing quote keeps this from matching them.
36
+ const RE_BICEP_MIN_TLS = /(?<![\w])minimumTlsVersion\s*:\s*'TLS1_[01]'/g;
37
+
38
+ const RULE_BICEP_KTY_RSA: RuleMeta = {
39
+ id: "bicep-keyvault-rsa",
40
+ title: "Bicep Azure Key Vault RSA key",
41
+ description: "Azure Bicep Microsoft.KeyVault key with kty: 'RSA'",
42
+ category: "kem",
43
+ severity: "high",
44
+ confidence: "high",
45
+ algorithm: "RSA",
46
+ hndl: true,
47
+ cwe: CWE_BROKEN_CRYPTO,
48
+ message: "Bicep provisions a classical RSA Azure Key Vault key, which is not quantum-safe.",
49
+ remediation: "Plan migration to PQC (ML-KEM-768 for encryption, ML-DSA-65 for signatures).",
50
+ };
51
+ const RULE_BICEP_KTY_EC: RuleMeta = {
52
+ id: "bicep-keyvault-ec",
53
+ title: "Bicep Azure Key Vault EC key",
54
+ description: "Azure Bicep Microsoft.KeyVault key with kty: 'EC'",
55
+ category: "key-exchange",
56
+ severity: "high",
57
+ confidence: "high",
58
+ algorithm: "ECDH",
59
+ hndl: true,
60
+ cwe: CWE_BROKEN_CRYPTO,
61
+ message:
62
+ "Bicep provisions a classical EC Azure Key Vault key; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
63
+ remediation:
64
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
65
+ };
66
+ const RULE_BICEP_MIN_TLS: RuleMeta = {
67
+ id: "bicep-min-tls-legacy",
68
+ title: "Bicep legacy minimum TLS version",
69
+ description: "Azure Bicep resource pins minimumTlsVersion to TLS 1.0/1.1",
70
+ category: "tls",
71
+ severity: "medium",
72
+ confidence: "high",
73
+ hndl: false,
74
+ cwe: CWE_WEAK_STRENGTH,
75
+ message: "Bicep resource pins a deprecated TLS floor (TLS 1.0/1.1); require TLS 1.2+ (1.3).",
76
+ remediation: "Set minimumTlsVersion: 'TLS1_2' and prefer PQC-hybrid key exchange as it lands.",
77
+ };
78
+
79
+ /** Detects classical asymmetric crypto and legacy TLS declared in Azure Bicep. */
80
+ export const bicepDetector: Detector = {
81
+ id: "bicep-crypto",
82
+ description: "Classical asymmetric crypto and legacy TLS declared in Azure Bicep (IaC)",
83
+ scope: "config",
84
+ language: "any",
85
+ rules: [RULE_BICEP_KTY_RSA, RULE_BICEP_KTY_EC, RULE_BICEP_MIN_TLS],
86
+ appliesTo: (f) => hasExtension(f, BICEP_EXTENSIONS),
87
+ detect({ file, content }): Finding[] {
88
+ // Bicep comments are `//` and `/* … */`; mask block comments (multi-line) then
89
+ // whole `//` comment lines so a commented-out resource can't fire. Offsets preserved.
90
+ const scan = maskCommentLines(maskBlockComments(content), ["//"]);
91
+ const findings: Finding[] = [];
92
+ const add = (re: RegExp, rule: RuleMeta) =>
93
+ eachMatch(re, scan, (m) =>
94
+ findings.push(
95
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
96
+ ),
97
+ );
98
+ // The bare `kty` field is generic; only treat it as a Key Vault key when the
99
+ // resource type is actually present, mirroring the cloudformation ARM gate.
100
+ if (content.includes("Microsoft.KeyVault")) {
101
+ add(RE_BICEP_KTY_RSA, RULE_BICEP_KTY_RSA);
102
+ add(RE_BICEP_KTY_EC, RULE_BICEP_KTY_EC);
103
+ }
104
+ add(RE_BICEP_MIN_TLS, RULE_BICEP_MIN_TLS);
105
+ return findings;
106
+ },
107
+ };
@@ -48,7 +48,9 @@ const RE_C_RSA_VERIFY = /\bRSA_verify\s*\(/g;
48
48
  const RE_C_RSA_CRYPT = /\bRSA_public_encrypt\s*\(|\bRSA_private_decrypt\s*\(/g;
49
49
  // C/OpenSSL legacy TLS configuration (mirrors source.ts tlsDetector): forcing a
50
50
  // deprecated protocol version or disabling certificate verification.
51
- const RE_C_TLS_VERSION = /\bTLSv1_method\b|\bSSLv3_method\b/g;
51
+ // TLSv1_method / TLSv1_1_method (+ the client/server-specific variants) and the
52
+ // SSLv2/SSLv3 method constructors — all pin a deprecated protocol (RFC 8996).
53
+ const RE_C_TLS_VERSION = /\b(?:TLSv1(?:_1)?|SSLv2|SSLv3)(?:_client|_server)?_method\b/g;
52
54
  const RE_C_TLS_VERIFY_NONE = /\bSSL_VERIFY_NONE\b/g;
53
55
  // Embedded C crypto libraries (the "embedded" niche): Mbed TLS and wolfSSL /
54
56
  // wolfCrypt. Both use highly-distinctive `mbedtls_*` / `wc_*` prefixes, so the
@@ -511,9 +513,35 @@ export const cDetector: Detector = {
511
513
  add(RE_C_DSA, RULE_C_DSA);
512
514
  add(RE_C_DH, RULE_C_DH);
513
515
  add(RE_C_EVP_KEYGEN, RULE_C_EVP_KEYGEN);
514
- add(RE_C_EVP_DERIVE, RULE_C_EVP_DERIVE);
516
+ // `EVP_PKEY_derive` is OpenSSL's entry point for BOTH (EC)DH key agreement and the
517
+ // symmetric KDFs (HKDF/scrypt/TLS1-PRF). Real (EC)DH always sets a peer key via
518
+ // `EVP_PKEY_derive_set_peer`; a KDF never does. So only treat it as classical key
519
+ // agreement when a peer is set, or when no KDF context marker is present.
520
+ const hasKdfCtx = /\bEVP_PKEY_(?:HKDF|SCRYPT|TLS1_PRF)\b/.test(content);
521
+ if (content.includes("EVP_PKEY_derive_set_peer") || !hasKdfCtx) {
522
+ add(RE_C_EVP_DERIVE, RULE_C_EVP_DERIVE);
523
+ }
515
524
  add(RE_C_EVP_CRYPT, RULE_C_EVP_CRYPT);
516
- add(RE_C_EVP_SIGN, RULE_C_EVP_SIGN);
525
+ // `EVP_DigestSign*` also drives HMAC/CMAC (a symmetric MAC keyed by
526
+ // `EVP_PKEY_new_mac_key(EVP_PKEY_HMAC/CMAC, …)`), which is NOT a classical
527
+ // asymmetric signature. Suppress a `DigestSign*` match ONLY when a MAC key setup
528
+ // sits just before it — per-match, so a real RSA/ECDSA sign elsewhere in the same
529
+ // file is still reported. `DigestVerify*` (no MAC form here) is never suppressed.
530
+ eachMatch(RE_C_EVP_SIGN, content, (m) => {
531
+ const isSign = m[0].includes("DigestSign");
532
+ if (isSign) {
533
+ const back = content.slice(Math.max(0, m.index - 300), m.index);
534
+ if (/EVP_PKEY_new_mac_key|EVP_PKEY_(?:HMAC|CMAC)\b/.test(back)) return;
535
+ }
536
+ findings.push(
537
+ findingFromRule(RULE_C_EVP_SIGN, {
538
+ file,
539
+ content,
540
+ index: m.index,
541
+ matchLength: m[0].length,
542
+ }),
543
+ );
544
+ });
517
545
  add(RE_C_SODIUM_BOX, RULE_C_SODIUM_BOX);
518
546
  add(RE_C_SODIUM_SIGN, RULE_C_SODIUM_SIGN);
519
547
  add(RE_C_ECDSA_VERIFY, RULE_C_ECDSA_VERIFY);
@@ -22,7 +22,7 @@
22
22
  * doc does not fire; the surface here is specifically the release pipeline.
23
23
  */
24
24
  import type { Detector, Finding, RuleMeta } from "../types.js";
25
- import { eachMatch, findingFromRule } from "../detect-utils.js";
25
+ import { eachMatch, findingFromRule, maskCommentLines } from "../detect-utils.js";
26
26
  import { CWE_BROKEN_CRYPTO } from "../cwe.js";
27
27
 
28
28
  /** True for the CI/CD pipeline definition files this detector inspects. */
@@ -48,7 +48,9 @@ interface CiRule {
48
48
 
49
49
  const CI_RULES: CiRule[] = [
50
50
  {
51
- re: /\bcosign\s+(?:sign|attest|sign-blob|generate-key-pair)\b/g,
51
+ // `sign-blob` precedes `sign` so the longer subcommand wins — otherwise `sign`
52
+ // matches first and the trailing `\b` succeeds at the `-`, never reaching `sign-blob`.
53
+ re: /\bcosign\s+(?:sign-blob|sign|attest|generate-key-pair)\b/g,
52
54
  meta: {
53
55
  id: "ci-cosign-ecdsa",
54
56
  title: "cosign artifact signing (ECDSA)",
@@ -66,7 +68,11 @@ const CI_RULES: CiRule[] = [
66
68
  },
67
69
  },
68
70
  {
69
- re: /\bgpg\b[^\n]*?\s--(?:detach-sign|clearsign|sign)\b/g,
71
+ // Bound the span to the gpg invocation ([^\n&|;] stops it crossing `&&`/`|`/`;`
72
+ // into another command's flag), and `(?![\w-])` stops `--sign` matching the
73
+ // `--sign` prefix of an unrelated flag like `--sign-artifacts`. The short forms
74
+ // `-s` (sign) / `-b` (detach-sign) are safe inside the bounded gpg span.
75
+ re: /\bgpg\b[^\n&|;]{0,120}?\s(?:-[sb]\b|--(?:detach-sign|clearsign|sign)(?![\w-]))/g,
70
76
  meta: {
71
77
  id: "ci-gpg-sign",
72
78
  title: "GPG signing (RSA)",
@@ -101,7 +107,10 @@ const CI_RULES: CiRule[] = [
101
107
  },
102
108
  },
103
109
  {
104
- re: /\bcodesign\s+(?:-s\b|--sign\b)/g,
110
+ // Allow intervening flags (the common `codesign --force --options runtime --sign`
111
+ // form), bounded to the codesign invocation so it can't latch onto a later
112
+ // command's `--sign` across `&&`/`|`/`;`.
113
+ re: /\bcodesign\b[^\n&|;]{0,120}?\s(?:-s\b|--sign\b)/g,
105
114
  meta: {
106
115
  id: "ci-codesign",
107
116
  title: "Apple codesign (RSA)",
@@ -146,8 +155,12 @@ export const cicdDetector: Detector = {
146
155
  appliesTo: isCiPipelineFile,
147
156
  detect({ file, content }): Finding[] {
148
157
  const findings: Finding[] = [];
158
+ // A commented-out CI step (`# - run: cosign sign …`, or a `//` line in a
159
+ // Jenkinsfile) is not an active signing step. Mask comment lines first; offsets
160
+ // are preserved so the snippet from the original `content` stays correct.
161
+ const scan = maskCommentLines(content, ["#", "//"]);
149
162
  for (const rule of CI_RULES) {
150
- eachMatch(rule.re, content, (m) => {
163
+ eachMatch(rule.re, scan, (m) => {
151
164
  findings.push(
152
165
  findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),
153
166
  );