@quantakrypto/core 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/README.md +76 -27
  2. package/dist/cbom-merge.d.ts.map +1 -1
  3. package/dist/cbom-merge.js +30 -8
  4. package/dist/cbom-merge.js.map +1 -1
  5. package/dist/cbom.d.ts +5 -0
  6. package/dist/cbom.d.ts.map +1 -1
  7. package/dist/cbom.js +132 -37
  8. package/dist/cbom.js.map +1 -1
  9. package/dist/comments.d.ts +2 -23
  10. package/dist/comments.d.ts.map +1 -1
  11. package/dist/comments.js +24 -5
  12. package/dist/comments.js.map +1 -1
  13. package/dist/detect-utils.d.ts +55 -21
  14. package/dist/detect-utils.d.ts.map +1 -1
  15. package/dist/detect-utils.js +133 -7
  16. package/dist/detect-utils.js.map +1 -1
  17. package/dist/detectors/age.d.ts +16 -0
  18. package/dist/detectors/age.d.ts.map +1 -0
  19. package/dist/detectors/age.js +40 -0
  20. package/dist/detectors/age.js.map +1 -0
  21. package/dist/detectors/ansible.d.ts +13 -0
  22. package/dist/detectors/ansible.d.ts.map +1 -0
  23. package/dist/detectors/ansible.js +108 -0
  24. package/dist/detectors/ansible.js.map +1 -0
  25. package/dist/detectors/bicep.d.ts +20 -0
  26. package/dist/detectors/bicep.d.ts.map +1 -0
  27. package/dist/detectors/bicep.js +74 -0
  28. package/dist/detectors/bicep.js.map +1 -0
  29. package/dist/detectors/c.d.ts.map +1 -1
  30. package/dist/detectors/c.js +30 -3
  31. package/dist/detectors/c.js.map +1 -1
  32. package/dist/detectors/cicd.d.ts.map +1 -1
  33. package/dist/detectors/cicd.js +18 -5
  34. package/dist/detectors/cicd.js.map +1 -1
  35. package/dist/detectors/cloud-kms.d.ts +8 -5
  36. package/dist/detectors/cloud-kms.d.ts.map +1 -1
  37. package/dist/detectors/cloud-kms.js +62 -20
  38. package/dist/detectors/cloud-kms.js.map +1 -1
  39. package/dist/detectors/cloudformation.d.ts +8 -0
  40. package/dist/detectors/cloudformation.d.ts.map +1 -1
  41. package/dist/detectors/cloudformation.js +41 -10
  42. package/dist/detectors/cloudformation.js.map +1 -1
  43. package/dist/detectors/codesign.d.ts +50 -0
  44. package/dist/detectors/codesign.d.ts.map +1 -0
  45. package/dist/detectors/codesign.js +143 -0
  46. package/dist/detectors/codesign.js.map +1 -0
  47. package/dist/detectors/csharp.d.ts.map +1 -1
  48. package/dist/detectors/csharp.js +13 -3
  49. package/dist/detectors/csharp.js.map +1 -1
  50. package/dist/detectors/dart.d.ts +35 -0
  51. package/dist/detectors/dart.d.ts.map +1 -0
  52. package/dist/detectors/dart.js +177 -0
  53. package/dist/detectors/dart.js.map +1 -0
  54. package/dist/detectors/database.d.ts +5 -3
  55. package/dist/detectors/database.d.ts.map +1 -1
  56. package/dist/detectors/database.js +45 -8
  57. package/dist/detectors/database.js.map +1 -1
  58. package/dist/detectors/dkim.d.ts +54 -0
  59. package/dist/detectors/dkim.d.ts.map +1 -0
  60. package/dist/detectors/dkim.js +92 -0
  61. package/dist/detectors/dkim.js.map +1 -0
  62. package/dist/detectors/dnssec.d.ts.map +1 -1
  63. package/dist/detectors/dnssec.js +19 -10
  64. package/dist/detectors/dnssec.js.map +1 -1
  65. package/dist/detectors/elixir.d.ts.map +1 -1
  66. package/dist/detectors/elixir.js +34 -2
  67. package/dist/detectors/elixir.js.map +1 -1
  68. package/dist/detectors/java.d.ts.map +1 -1
  69. package/dist/detectors/java.js +33 -3
  70. package/dist/detectors/java.js.map +1 -1
  71. package/dist/detectors/jose.d.ts.map +1 -1
  72. package/dist/detectors/jose.js +13 -3
  73. package/dist/detectors/jose.js.map +1 -1
  74. package/dist/detectors/jwk.d.ts +7 -7
  75. package/dist/detectors/jwk.d.ts.map +1 -1
  76. package/dist/detectors/jwk.js +44 -8
  77. package/dist/detectors/jwk.js.map +1 -1
  78. package/dist/detectors/k8s.d.ts.map +1 -1
  79. package/dist/detectors/k8s.js +5 -2
  80. package/dist/detectors/k8s.js.map +1 -1
  81. package/dist/detectors/keystore.d.ts +19 -0
  82. package/dist/detectors/keystore.d.ts.map +1 -0
  83. package/dist/detectors/keystore.js +76 -0
  84. package/dist/detectors/keystore.js.map +1 -0
  85. package/dist/detectors/mesh.d.ts +7 -8
  86. package/dist/detectors/mesh.d.ts.map +1 -1
  87. package/dist/detectors/mesh.js +17 -35
  88. package/dist/detectors/mesh.js.map +1 -1
  89. package/dist/detectors/messaging.d.ts +2 -2
  90. package/dist/detectors/messaging.d.ts.map +1 -1
  91. package/dist/detectors/messaging.js +19 -12
  92. package/dist/detectors/messaging.js.map +1 -1
  93. package/dist/detectors/objc.d.ts +41 -0
  94. package/dist/detectors/objc.d.ts.map +1 -0
  95. package/dist/detectors/objc.js +146 -0
  96. package/dist/detectors/objc.js.map +1 -0
  97. package/dist/detectors/openpgp.d.ts +20 -0
  98. package/dist/detectors/openpgp.d.ts.map +1 -0
  99. package/dist/detectors/openpgp.js +151 -0
  100. package/dist/detectors/openpgp.js.map +1 -0
  101. package/dist/detectors/pem.d.ts.map +1 -1
  102. package/dist/detectors/pem.js +32 -0
  103. package/dist/detectors/pem.js.map +1 -1
  104. package/dist/detectors/php.d.ts +2 -1
  105. package/dist/detectors/php.d.ts.map +1 -1
  106. package/dist/detectors/php.js +7 -3
  107. package/dist/detectors/php.js.map +1 -1
  108. package/dist/detectors/pkcs11.d.ts +17 -0
  109. package/dist/detectors/pkcs11.d.ts.map +1 -0
  110. package/dist/detectors/pkcs11.js +87 -0
  111. package/dist/detectors/pkcs11.js.map +1 -0
  112. package/dist/detectors/proxy.d.ts +52 -0
  113. package/dist/detectors/proxy.d.ts.map +1 -0
  114. package/dist/detectors/proxy.js +157 -0
  115. package/dist/detectors/proxy.js.map +1 -0
  116. package/dist/detectors/pulumi.d.ts +19 -0
  117. package/dist/detectors/pulumi.d.ts.map +1 -0
  118. package/dist/detectors/pulumi.js +80 -0
  119. package/dist/detectors/pulumi.js.map +1 -0
  120. package/dist/detectors/rust.d.ts.map +1 -1
  121. package/dist/detectors/rust.js +5 -0
  122. package/dist/detectors/rust.js.map +1 -1
  123. package/dist/detectors/secrets.d.ts +4 -1
  124. package/dist/detectors/secrets.d.ts.map +1 -1
  125. package/dist/detectors/secrets.js +5 -21
  126. package/dist/detectors/secrets.js.map +1 -1
  127. package/dist/detectors/solidity.d.ts +50 -0
  128. package/dist/detectors/solidity.d.ts.map +1 -0
  129. package/dist/detectors/solidity.js +108 -0
  130. package/dist/detectors/solidity.js.map +1 -0
  131. package/dist/detectors/source.d.ts.map +1 -1
  132. package/dist/detectors/source.js +53 -6
  133. package/dist/detectors/source.js.map +1 -1
  134. package/dist/detectors/spire.d.ts +49 -0
  135. package/dist/detectors/spire.d.ts.map +1 -0
  136. package/dist/detectors/spire.js +77 -0
  137. package/dist/detectors/spire.js.map +1 -0
  138. package/dist/detectors/ssh-ca.d.ts +72 -0
  139. package/dist/detectors/ssh-ca.d.ts.map +1 -0
  140. package/dist/detectors/ssh-ca.js +119 -0
  141. package/dist/detectors/ssh-ca.js.map +1 -0
  142. package/dist/detectors/supply-chain.d.ts +17 -0
  143. package/dist/detectors/supply-chain.d.ts.map +1 -0
  144. package/dist/detectors/supply-chain.js +89 -0
  145. package/dist/detectors/supply-chain.js.map +1 -0
  146. package/dist/detectors/swift.d.ts +22 -0
  147. package/dist/detectors/swift.d.ts.map +1 -0
  148. package/dist/detectors/swift.js +120 -0
  149. package/dist/detectors/swift.js.map +1 -0
  150. package/dist/detectors/terraform.d.ts.map +1 -1
  151. package/dist/detectors/terraform.js +35 -8
  152. package/dist/detectors/terraform.js.map +1 -1
  153. package/dist/detectors/vault.d.ts +15 -0
  154. package/dist/detectors/vault.d.ts.map +1 -0
  155. package/dist/detectors/vault.js +97 -0
  156. package/dist/detectors/vault.js.map +1 -0
  157. package/dist/detectors/vpn.d.ts +8 -8
  158. package/dist/detectors/vpn.d.ts.map +1 -1
  159. package/dist/detectors/vpn.js +12 -38
  160. package/dist/detectors/vpn.js.map +1 -1
  161. package/dist/detectors/weak-hash.d.ts +71 -0
  162. package/dist/detectors/weak-hash.d.ts.map +1 -0
  163. package/dist/detectors/weak-hash.js +116 -0
  164. package/dist/detectors/weak-hash.js.map +1 -0
  165. package/dist/detectors/webauthn.d.ts +61 -0
  166. package/dist/detectors/webauthn.d.ts.map +1 -0
  167. package/dist/detectors/webauthn.js +109 -0
  168. package/dist/detectors/webauthn.js.map +1 -0
  169. package/dist/detectors/xmldsig.d.ts +23 -0
  170. package/dist/detectors/xmldsig.d.ts.map +1 -0
  171. package/dist/detectors/xmldsig.js +84 -0
  172. package/dist/detectors/xmldsig.js.map +1 -0
  173. package/dist/evidence.d.ts +74 -4
  174. package/dist/evidence.d.ts.map +1 -1
  175. package/dist/evidence.js +71 -0
  176. package/dist/evidence.js.map +1 -1
  177. package/dist/index.d.ts +14 -11
  178. package/dist/index.d.ts.map +1 -1
  179. package/dist/index.js +17 -11
  180. package/dist/index.js.map +1 -1
  181. package/dist/inventory.d.ts +0 -2
  182. package/dist/inventory.d.ts.map +1 -1
  183. package/dist/inventory.js +15 -6
  184. package/dist/inventory.js.map +1 -1
  185. package/dist/registry.d.ts +9 -4
  186. package/dist/registry.d.ts.map +1 -1
  187. package/dist/registry.js +51 -4
  188. package/dist/registry.js.map +1 -1
  189. package/dist/remediate-pipeline.d.ts +0 -3
  190. package/dist/remediate-pipeline.d.ts.map +1 -1
  191. package/dist/remediate-pipeline.js +1 -1
  192. package/dist/remediate-pipeline.js.map +1 -1
  193. package/dist/remediation.d.ts +10 -0
  194. package/dist/remediation.d.ts.map +1 -1
  195. package/dist/remediation.js +32 -0
  196. package/dist/remediation.js.map +1 -1
  197. package/dist/report.d.ts +9 -0
  198. package/dist/report.d.ts.map +1 -1
  199. package/dist/report.js +31 -1
  200. package/dist/report.js.map +1 -1
  201. package/dist/scan-worker.js +5 -3
  202. package/dist/scan-worker.js.map +1 -1
  203. package/dist/scan.d.ts.map +1 -1
  204. package/dist/scan.js +35 -6
  205. package/dist/scan.js.map +1 -1
  206. package/dist/standards-profiles.d.ts +58 -0
  207. package/dist/standards-profiles.d.ts.map +1 -0
  208. package/dist/standards-profiles.js +100 -0
  209. package/dist/standards-profiles.js.map +1 -0
  210. package/dist/types.d.ts +6 -5
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/types.js.map +1 -1
  213. package/dist/version.d.ts +1 -1
  214. package/dist/version.js +1 -1
  215. package/dist/version.js.map +1 -1
  216. package/dist/vex.d.ts +39 -0
  217. package/dist/vex.d.ts.map +1 -0
  218. package/dist/vex.js +87 -0
  219. package/dist/vex.js.map +1 -0
  220. package/dist/walk.d.ts +8 -3
  221. package/dist/walk.d.ts.map +1 -1
  222. package/dist/walk.js +32 -1
  223. package/dist/walk.js.map +1 -1
  224. package/package.json +1 -1
  225. package/src/cbom-merge.ts +38 -8
  226. package/src/cbom.ts +153 -39
  227. package/src/comments.ts +25 -6
  228. package/src/detect-utils.ts +133 -11
  229. package/src/detectors/age.ts +60 -0
  230. package/src/detectors/ansible.ts +140 -0
  231. package/src/detectors/bicep.ts +107 -0
  232. package/src/detectors/c.ts +31 -3
  233. package/src/detectors/cicd.ts +18 -5
  234. package/src/detectors/cloud-kms.ts +79 -25
  235. package/src/detectors/cloudformation.ts +45 -10
  236. package/src/detectors/codesign.ts +214 -0
  237. package/src/detectors/csharp.ts +13 -3
  238. package/src/detectors/dart.ts +237 -0
  239. package/src/detectors/database.ts +64 -11
  240. package/src/detectors/dkim.ts +172 -0
  241. package/src/detectors/dnssec.ts +19 -10
  242. package/src/detectors/elixir.ts +33 -2
  243. package/src/detectors/java.ts +37 -3
  244. package/src/detectors/jose.ts +19 -3
  245. package/src/detectors/jwk.ts +59 -17
  246. package/src/detectors/k8s.ts +5 -2
  247. package/src/detectors/keystore.ts +109 -0
  248. package/src/detectors/mesh.ts +21 -45
  249. package/src/detectors/messaging.ts +22 -14
  250. package/src/detectors/objc.ts +209 -0
  251. package/src/detectors/openpgp.ts +194 -0
  252. package/src/detectors/pem.ts +30 -0
  253. package/src/detectors/php.ts +9 -4
  254. package/src/detectors/pkcs11.ts +113 -0
  255. package/src/detectors/proxy.ts +236 -0
  256. package/src/detectors/pulumi.ts +106 -0
  257. package/src/detectors/rust.ts +5 -0
  258. package/src/detectors/secrets.ts +15 -26
  259. package/src/detectors/solidity.ts +176 -0
  260. package/src/detectors/source.ts +48 -5
  261. package/src/detectors/spire.ts +143 -0
  262. package/src/detectors/ssh-ca.ts +215 -0
  263. package/src/detectors/supply-chain.ts +117 -0
  264. package/src/detectors/swift.ts +149 -0
  265. package/src/detectors/terraform.ts +42 -8
  266. package/src/detectors/vault.ts +120 -0
  267. package/src/detectors/vpn.ts +20 -48
  268. package/src/detectors/weak-hash.ts +217 -0
  269. package/src/detectors/webauthn.ts +205 -0
  270. package/src/detectors/xmldsig.ts +115 -0
  271. package/src/evidence.ts +127 -4
  272. package/src/index.ts +41 -13
  273. package/src/inventory.ts +15 -7
  274. package/src/registry.ts +51 -4
  275. package/src/remediate-pipeline.ts +1 -1
  276. package/src/remediation.ts +39 -0
  277. package/src/report.ts +36 -1
  278. package/src/scan-worker.ts +5 -3
  279. package/src/scan.ts +41 -6
  280. package/src/standards-profiles.ts +136 -0
  281. package/src/types.ts +9 -4
  282. package/src/version.ts +1 -1
  283. package/src/vex.ts +122 -0
  284. package/src/walk.ts +33 -2
@@ -32,6 +32,7 @@ import {
32
32
  DOC_EXTENSIONS,
33
33
  JWT_HOST_EXTENSIONS,
34
34
  eachMatch,
35
+ enclosingObject,
35
36
  findingFromRule,
36
37
  hasExtension,
37
38
  nearSortedCall,
@@ -225,11 +226,15 @@ const RE_TLS_REJECT = /rejectUnauthorized\s*:\s*false/g;
225
226
  // Hardened cipher regex: bounded spans (no unbounded `[^'"`]*` straddling the
226
227
  // alternation), single-quote-style anchoring removed in favour of {0,256} bounds
227
228
  // so worst-case backtracking is linear in the bound, not the file (P0-6).
228
- // The `(?<![!-])` lookbehind skips OpenSSL EXCLUSION syntax — `!MD5` / `-RC4`
229
- // DISABLE those ciphers, so a hardened list like `...:!aNULL:!MD5:!RC4` must not
230
- // be flagged as weak (audit: crypto #7). A genuinely-enabled `RC4` still matches.
229
+ // Skip OpenSSL EXCLUSION syntax — `!MD5` / `:-RC4` / `!ECDHE-RSA-RC4-SHA` DISABLE
230
+ // those ciphers, so a hardened list must not be flagged as weak (audit: crypto #7). The
231
+ // single variable-length lookbehind walks back over the suite-name chars to the
232
+ // ELEMENT boundary (`:` / whitespace / comma / quote / start) and rejects the match
233
+ // when that element begins with `!` or `-` — covering a bare `!RC4`, a space-separated
234
+ // `HIGH !RC4`, and a full-suite `!ECDHE-RSA-RC4-SHA`. The intra-name hyphen in a
235
+ // genuinely-enabled `ECDHE-RSA-RC4-SHA` (no leading `!`/`-`) still matches.
231
236
  const RE_TLS_WEAK_CIPHER =
232
- /ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![!-])(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
237
+ /ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![:'"`,\s]\s*[!-][\w-]{0,64})(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
233
238
 
234
239
  /* -------------------------------------------------------------------------- */
235
240
  /* Node.js `crypto` module */
@@ -561,7 +566,9 @@ const webCryptoDetector: Detector = {
561
566
  algorithm = name === "X448" ? "X448" : "X25519";
562
567
  category = "key-exchange";
563
568
  hndl = true;
564
- severity = "low"; // modern but classical
569
+ // `medium`, aligned with X25519/X448 key agreement in node/rust/go — HNDL-exposed
570
+ // key agreement scores the same regardless of which surface uses it.
571
+ severity = "medium";
565
572
  } else if (name === "ED25519" || name === "ED448") {
566
573
  algorithm = "EdDSA";
567
574
  category = "signature";
@@ -818,8 +825,21 @@ const jwtDetector: Detector = {
818
825
  detect({ file, content }): Finding[] {
819
826
  const findings: Finding[] = [];
820
827
 
828
+ // Deferral guards against double-counting:
829
+ // - webCryptoDetector owns a quoted `RSA-OAEP`/`ECDH-ES` sitting next to a
830
+ // `subtle.*(` call (it matches the `RSA-OAEP`/`ECDH` name). Collect the subtle
831
+ // call offsets so the JOSE key-management loops can skip those. (In non-JS host
832
+ // files there are no subtle calls, so nothing is skipped.)
833
+ // - jwkDetector owns any alg/`kty` that lives inside a JWK object; skip a token
834
+ // whose enclosing `{…}` carries a `"kty"` so a JWK's own `alg` is not counted by
835
+ // both detectors. Mirrors the same guard jose.ts applies for other languages.
836
+ const subtleCalls: number[] = [];
837
+ eachMatch(RE_SUBTLE_CALL, content, (m) => subtleCalls.push(m.index));
838
+ const inJwk = (index: number): boolean => enclosingObject(content, index).includes('"kty"');
839
+
821
840
  // Classical JWS signature alg tokens. Anchored to quotes to avoid words.
822
841
  eachMatch(RE_JWT_ALG, content, (m) => {
842
+ if (inJwk(m.index)) return; // jwk-{rsa,ec,eddsa} owns a JWK's own alg
823
843
  const alg = m[1];
824
844
  let algorithm: Finding["algorithm"];
825
845
  if (alg.startsWith("RS") || alg.startsWith("PS")) algorithm = "RSA";
@@ -840,6 +860,8 @@ const jwtDetector: Detector = {
840
860
 
841
861
  // JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
842
862
  eachMatch(RE_JOSE_ECDH, content, (m) => {
863
+ if (inJwk(m.index)) return; // jwk-ec owns a JWK's own alg
864
+ if (nearSortedCall(subtleCalls, m.index, 400)) return; // webcrypto owns it
843
865
  findings.push(
844
866
  findingFromRule(
845
867
  RULE_JOSE_ECDH,
@@ -854,6 +876,10 @@ const jwtDetector: Detector = {
854
876
 
855
877
  // JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
856
878
  eachMatch(RE_JOSE_KEM, content, (m) => {
879
+ if (inJwk(m.index)) return; // jwk-rsa owns a JWK's own alg
880
+ // webCryptoDetector's regex matches `RSA-OAEP` but NOT `RSA1_5`, so only defer the
881
+ // OAEP form near a subtle call — deferring RSA1_5 would drop it entirely.
882
+ if (m[1].startsWith("RSA-OAEP") && nearSortedCall(subtleCalls, m.index, 400)) return;
857
883
  findings.push(
858
884
  findingFromRule(
859
885
  RULE_JOSE_RSA_OAEP,
@@ -981,6 +1007,21 @@ const RE_CERT_SIG_ALG =
981
1007
  const RE_SSH_KEX =
982
1008
  /\b(diffie-hellman-group(?:1|14|15|16|17|18)(?:-sha1|-sha256|-sha512)?|diffie-hellman-group-exchange-sha(?:1|256)|ecdh-sha2-nistp(?:256|384|521)|curve25519-sha256)\b/g;
983
1009
 
1010
+ /**
1011
+ * True when the match at `index` sits inside a DOCUMENTATION field value — a
1012
+ * `description` / `help` / `summary` / `doc` / `comment` / `note` assignment
1013
+ * (`key = "…"` or `key: "…"`), as used by Terraform/Packer variable descriptions,
1014
+ * JSON-schema docs, etc. Classical KEX/cipher algorithm names LISTED in such a field
1015
+ * are prose ("acceptable values include ecdh-sha2-nistp256, …"), not an active
1016
+ * setting, so a transport-token rule must not fire on them. Scans the current line's
1017
+ * prefix only (cheap; the common single-line description case).
1018
+ */
1019
+ function inDocFieldValue(content: string, index: number): boolean {
1020
+ const lineStart = content.lastIndexOf("\n", index - 1) + 1;
1021
+ const prefix = content.slice(lineStart, index);
1022
+ return /\b(?:description|help|summary|doc|comment|note)\b\s*[:=]/i.test(prefix);
1023
+ }
1024
+
984
1025
  // A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
985
1026
  // when it is EITHER (a) followed by base64 key material — an actual
986
1027
  // authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
@@ -1128,6 +1169,7 @@ const sshCertDetector: Detector = {
1128
1169
 
1129
1170
  // SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
1130
1171
  eachMatch(RE_SSH_KEX, content, (m) => {
1172
+ if (inDocFieldValue(content, m.index)) return; // names listed in a doc string
1131
1173
  const tok = m[1];
1132
1174
  const algorithm: Finding["algorithm"] = tok.startsWith("diffie-hellman")
1133
1175
  ? "DH"
@@ -1196,6 +1238,7 @@ const tlsClassicalKexDetector: Detector = {
1196
1238
  detect({ file, content }): Finding[] {
1197
1239
  const findings: Finding[] = [];
1198
1240
  eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
1241
+ if (inDocFieldValue(content, m.index)) return; // names listed in a doc string
1199
1242
  const tok = m[0];
1200
1243
  const algorithm: Finding["algorithm"] = tok.includes("ECDH") ? "ECDH" : "DH";
1201
1244
  findings.push(
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Config detector: classical key types configured for SPIFFE X.509 SVIDs in
3
+ * SPIRE server/agent configuration (HCL, and the `.conf`/YAML/JSON variants
4
+ * SPIRE deployments ship).
5
+ *
6
+ * SPIRE is the reference SPIFFE implementation: it issues each workload a
7
+ * short-lived X.509-SVID certificate, signed by the SPIRE server's CA (itself
8
+ * often chained to an UpstreamAuthority). The SVID leaf key and the CA signing
9
+ * key are configured by explicit key-type strings:
10
+ * - server `ca_key_type = "rsa-2048" | "rsa-4096" | "ec-p256" | "ec-p384"` —
11
+ * the CA that signs every workload SVID in the trust domain (the identity
12
+ * ROOT: forge this and you forge the whole trust domain).
13
+ * - agent/server `svid_key_type = "rsa-2048" | "ec-p256"` — the per-workload
14
+ * leaf key type.
15
+ * - per-plugin `key_type = "rsa-2048" | "ec-p256"` on UpstreamAuthority /
16
+ * KeyManager plugins.
17
+ *
18
+ * These are SIGNING / identity keys, not confidentiality keys: an SVID X.509
19
+ * signature is verified, never used to wrap harvestable ciphertext. So there is
20
+ * nothing to "harvest now, decrypt later" — `hndl: false` — but every SVID
21
+ * signature (and the CA signature over it) becomes FORGEABLE the moment a
22
+ * cryptographically-relevant quantum computer (CRQC) exists, letting an attacker
23
+ * mint certificates for any SPIFFE ID in the trust domain. Findings are
24
+ * therefore `category: "certificate"` (X.509 / PKI material), `severity:
25
+ * "medium"`, `confidence: "high"`.
26
+ *
27
+ * Algorithm mapping (SPIRE key-type value → {@link AlgorithmFamily}):
28
+ * - `rsa-2048` / `rsa-4096` / any `rsa-*` → RSA
29
+ * - `ec-p256` / `ec-p384` / `ecdsa` → ECDSA
30
+ *
31
+ * Match shape: the key-type attribute name (`ca_key_type`, `svid_key_type`, or
32
+ * the bare per-plugin `key_type`) followed by `=`/`:` and a quoted value, in
33
+ * both HCL (`=`) and YAML/JSON (`:`) forms. The `\b(?:ca_|svid_)?key_type\b`
34
+ * anchor covers all three names in one pattern — the leading `\b` won't fire
35
+ * inside `ca_key_type`/`svid_key_type` for the bare `key_type` alternative
36
+ * because `_` is a word character, so those longer names are matched whole.
37
+ *
38
+ * Fast reject: `detect()` bails unless the file carries a SPIFFE/SPIRE-specific
39
+ * marker (`spiffe`, `spire`, the distinctive `ca_key_type`, or `svid`). A bare
40
+ * `key_type = "rsa-2048"` is deliberately NOT a sufficient marker — `key_type`
41
+ * is a generic attribute (Vault PKI roles, cloud IaC, …), so without a
42
+ * spiffe/spire/svid/ca_key_type anchor an unrelated config must not fire. This
43
+ * keeps the generic per-plugin `key_type` rule precise while still catching a
44
+ * real SPIRE config, which always names its trust domain / server / SVIDs.
45
+ */
46
+ import type { Detector, Finding, RuleMeta } from "../types.js";
47
+ import {
48
+ DOC_EXTENSIONS,
49
+ eachMatch,
50
+ findingFromRule,
51
+ hasExtension,
52
+ maskCommentLines,
53
+ } from "../detect-utils.js";
54
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
55
+
56
+ // SPIRE config is HCL/conf, but deployments also template it as YAML (Helm
57
+ // values / ConfigMaps) or JSON. Prose (`.md`, …) is excluded in `appliesTo`.
58
+ const SPIRE_EXTENSIONS: readonly string[] = [".conf", ".hcl", ".yaml", ".yml", ".json"];
59
+
60
+ // The three SPIRE key-type attribute names in one anchor: `ca_key_type`,
61
+ // `svid_key_type`, and the bare per-plugin `key_type`.
62
+ const KEY_ATTR = "(?:ca_|svid_)?key_type";
63
+ // HCL `=`, unquoted YAML `:`, and JSON `"key_type": "..."` assignment forms. The
64
+ // attribute-name and value quotes are BOTH optional (`"?`): HCL uses `key_type =
65
+ // "rsa-2048"`, Helm/YAML uses the unquoted `key_type: rsa-2048`, and JSON quotes
66
+ // the key (`"key_type": "rsa-2048"`) — a required-quote regex missed the latter
67
+ // two entirely. Case-insensitive, though SPIRE writes these lowercase.
68
+ const RE_SPIRE_RSA = new RegExp(`\\b${KEY_ATTR}"?\\s*[:=]\\s*"?rsa-\\d+`, "gi");
69
+ const RE_SPIRE_ECDSA = new RegExp(`\\b${KEY_ATTR}"?\\s*[:=]\\s*"?(?:ec-p\\d+|ecdsa)\\b`, "gi");
70
+
71
+ const RULE_SPIRE_RSA: RuleMeta = {
72
+ id: "spire-rsa-key",
73
+ title: "SPIRE/SPIFFE RSA SVID key type",
74
+ description: 'SPIRE ca_key_type/svid_key_type/key_type = "rsa-*" (classical X.509-SVID key)',
75
+ category: "certificate",
76
+ severity: "medium",
77
+ confidence: "high",
78
+ algorithm: "RSA",
79
+ hndl: false,
80
+ cwe: CWE_BROKEN_CRYPTO,
81
+ message:
82
+ "SPIRE issues SPIFFE X.509-SVIDs with a classical RSA key; the SVID (and CA) signatures become forgeable once a CRQC exists, letting an attacker mint identities for the trust domain.",
83
+ remediation:
84
+ "SPIFFE/SPIRE has no PQC SVID key type yet — track the SPIFFE roadmap for PQC signature support. Prioritise the SPIRE server ca_key_type: the classical CA is the identity root, so its forgeability compromises every workload SVID.",
85
+ };
86
+ const RULE_SPIRE_ECDSA: RuleMeta = {
87
+ id: "spire-ec-key",
88
+ title: "SPIRE/SPIFFE ECDSA SVID key type",
89
+ description:
90
+ 'SPIRE ca_key_type/svid_key_type/key_type = "ec-p256|ec-p384|ecdsa" (classical X.509-SVID key)',
91
+ category: "certificate",
92
+ severity: "medium",
93
+ confidence: "high",
94
+ algorithm: "ECDSA",
95
+ hndl: false,
96
+ cwe: CWE_BROKEN_CRYPTO,
97
+ message:
98
+ "SPIRE issues SPIFFE X.509-SVIDs with a classical ECDSA (P-256/P-384) key; the SVID (and CA) signatures become forgeable once a CRQC exists, letting an attacker mint identities for the trust domain.",
99
+ remediation:
100
+ "SPIFFE/SPIRE has no PQC SVID key type yet — track the SPIFFE roadmap for PQC signature support. Prioritise the SPIRE server ca_key_type: the classical CA is the identity root, so its forgeability compromises every workload SVID.",
101
+ };
102
+
103
+ /**
104
+ * True when `content` carries a SPIFFE/SPIRE-specific marker. A bare generic
105
+ * `key_type` is intentionally NOT enough — only `spiffe`/`spire` context, the
106
+ * distinctive `ca_key_type`, or an `svid` token qualifies — so an unrelated
107
+ * config that merely sets `key_type = "rsa-2048"` cannot fire.
108
+ */
109
+ function hasSpireMarker(content: string): boolean {
110
+ // Word-anchored so `inspired` / `aspire` / a `svid` substring inside a longer
111
+ // token don't qualify; `ca_key_type` is distinctive enough to match unanchored.
112
+ return /\b(?:spiffe|spire|svid)\b/i.test(content) || content.includes("ca_key_type");
113
+ }
114
+
115
+ /** Detects classical key types configured for SPIFFE X.509 SVIDs in SPIRE config. */
116
+ export const spireDetector: Detector = {
117
+ id: "spire-crypto",
118
+ description: "Classical key types for SPIFFE X.509 SVIDs in SPIRE server/agent config",
119
+ scope: "config",
120
+ language: "any",
121
+ rules: [RULE_SPIRE_RSA, RULE_SPIRE_ECDSA],
122
+ // Gate to SPIRE's config extensions, and never run on prose (a README that
123
+ // describes `ca_key_type = "rsa-2048"` is documentation, not live config).
124
+ appliesTo: (f) => hasExtension(f, SPIRE_EXTENSIONS) && !hasExtension(f, DOC_EXTENSIONS),
125
+ detect({ file, content }): Finding[] {
126
+ if (!hasSpireMarker(content)) return [];
127
+
128
+ // Mask whole comment lines (HCL/YAML `#`, HCL/JSON5 `//`) so a commented-out
129
+ // key-type directive can't fire. Offsets are preserved, so finding
130
+ // line/column/snippet for the remaining live config stay exact.
131
+ const scan = maskCommentLines(content, ["#", "//"]);
132
+ const findings: Finding[] = [];
133
+ const add = (re: RegExp, rule: RuleMeta) =>
134
+ eachMatch(re, scan, (m) =>
135
+ findings.push(
136
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
137
+ ),
138
+ );
139
+ add(RE_SPIRE_RSA, RULE_SPIRE_RSA);
140
+ add(RE_SPIRE_ECDSA, RULE_SPIRE_ECDSA);
141
+ return findings;
142
+ },
143
+ };
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Config detector: classical SSH **certificate-authority** cryptography — the
3
+ * OpenSSH certificate mechanism, in which a CA key signs host/user certificates
4
+ * (`*-cert-v01@openssh.com`) that servers and clients then trust in place of
5
+ * bare public keys.
6
+ *
7
+ * This is DIFFERENT from the `ssh-kex-classical` rule in `source.ts`, and from
8
+ * the `ssh-public-key` rule there — deliberately non-overlapping:
9
+ * - `ssh-kex-classical` is about the SSH *key-exchange* (finite-field DH, ECDH,
10
+ * curve25519): session-key *agreement*, `category: "key-exchange"`,
11
+ * `hndl: true` — a passively-recorded handshake is decryptable once a CRQC
12
+ * exists (harvest-now-decrypt-later confidentiality loss).
13
+ * - `ssh-public-key` flags bare `ssh-rsa AAAA…` / `ecdsa-sha2-* …` key lines.
14
+ * - THIS detector flags the SSH **certificate / CA signing** surface: the
15
+ * `*-cert-v01@openssh.com` cert key types, the `@cert-authority` known_hosts
16
+ * marker, the `TrustedUserCAKeys` / `HostCertificate` sshd directives, and
17
+ * `ssh-keygen -s` (the command that signs a certificate with a CA key).
18
+ *
19
+ * WHY it matters for PQC: an SSH certificate is a *signature* made by a CA key
20
+ * over a principal's public key + validity constraints. There is no
21
+ * confidentiality to harvest (so `hndl: false`), but every such signature
22
+ * becomes FORGEABLE the moment a cryptographically-relevant quantum computer
23
+ * (CRQC) can recover the classical CA private key from its public key. Forging a
24
+ * CA signature lets an attacker mint host or user certificates that every relying
25
+ * party accepts — a full trust-root compromise. SSH CA keys are also unusually
26
+ * long-lived (they are the anchor an entire fleet trusts and are rotated
27
+ * rarely), so the exposure window is large. Hence `category: "signature"`,
28
+ * `severity: "medium"`, `confidence: "high"`.
29
+ *
30
+ * Two match shapes:
31
+ * 1. The cert key type, which NAMES the signing algorithm → an accurate
32
+ * {@link AlgorithmFamily}:
33
+ * - `ssh-rsa-cert-v01@openssh.com`, `rsa-sha2-256/512-cert-v01@openssh.com` → RSA
34
+ * - `ecdsa-sha2-nistp256/384/521-cert-v01@openssh.com` → ECDSA
35
+ * - `ssh-ed25519-cert-v01@openssh.com` → EdDSA
36
+ * 2. The CA *deployment* directives — `TrustedUserCAKeys` / `HostCertificate`
37
+ * (sshd_config) and `ssh-keygen -s` (the CA signing command). These are the
38
+ * MOST COMMON way an SSH CA is configured, but they don't name the algorithm
39
+ * (that lives in the referenced key file), so they emit the `ssh-ca-config`
40
+ * rule with `algorithm: "unknown"` and `confidence: "medium"`.
41
+ *
42
+ * Precision:
43
+ * - Fast reject: `detect()` bails unless the file carries an SSH-CA marker
44
+ * (`cert-v01@openssh.com`, `@cert-authority`, `TrustedUserCAKeys`,
45
+ * `HostCertificate`, or `ssh-keygen -s`) — so the algorithm tokens can't be
46
+ * reached on unrelated config.
47
+ * - The cert-type tokens are the full `*-cert-v01@openssh.com` strings. The
48
+ * trailing `-cert-v01@openssh.com` suffix distinguishes a CA certificate type
49
+ * from the plain `ssh-rsa` / `ssh-ed25519` public-key tokens handled by
50
+ * `ssh-public-key`, so a non-cert `ssh-ed25519 AAAA…` line NEVER fires a cert
51
+ * rule here.
52
+ * - Comment lines (`#`) are masked with {@link maskCommentLines} (offsets
53
+ * preserved) so a commented-out cert line can't fire. Known_hosts
54
+ * `@cert-authority` lines start with `@`, not `#`, so masking leaves them
55
+ * live — exactly what we want.
56
+ * - Doc/prose files are excluded ({@link DOC_EXTENSIONS}), and so are program
57
+ * SOURCE files ({@link ANALYZABLE_SOURCE_EXTENSIONS}) — a vendored SSH library
58
+ * or constants table that spells a cert type as a string literal is not live
59
+ * CA config. SSH CA config lives in extensionless / `.conf` / `.pub` files.
60
+ *
61
+ * The matched line frequently carries the CA public key's base64 blob (a
62
+ * `@cert-authority` known_hosts entry), so findings are marked `sensitive` and
63
+ * reporters drop the snippet, consistent with `ssh-public-key`.
64
+ */
65
+ import type { Detector, Finding, RuleMeta } from "../types.js";
66
+ import {
67
+ eachMatch,
68
+ findingFromRule,
69
+ hasExtension,
70
+ maskCommentLines,
71
+ DOC_EXTENSIONS,
72
+ ANALYZABLE_SOURCE_EXTENSIONS,
73
+ } from "../detect-utils.js";
74
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
75
+
76
+ // --- OpenSSH certificate key types (`*-cert-v01@openssh.com`), by signing family. ---
77
+ // The `\b` after `.com` is a real boundary (`m` is a word char); the leading `\b`
78
+ // keeps `ssh-rsa-cert-v01@…` from matching inside a longer token. RSA covers the
79
+ // legacy `ssh-rsa` cert type plus the SHA-2 RSA cert types (`rsa-sha2-256/512`)
80
+ // OpenSSH emits for RSA CA/host certificates.
81
+ const RE_CERT_RSA = /\b(?:ssh-rsa|rsa-sha2-(?:256|512))-cert-v01@openssh\.com\b/g;
82
+ const RE_CERT_ECDSA = /\becdsa-sha2-nistp(?:256|384|521)-cert-v01@openssh\.com\b/g;
83
+ const RE_CERT_EDDSA = /\bssh-ed25519-cert-v01@openssh\.com\b/g;
84
+ // The canonical CA *deployment* directives — the most common way an SSH CA is
85
+ // configured (sshd_config `TrustedUserCAKeys` / `HostCertificate`, and the CA
86
+ // signing command `ssh-keygen -s`). These name the CA usage but not the algorithm
87
+ // (that lives in the referenced key file), so the finding is `algorithm: "unknown"`.
88
+ const RE_CA_DIRECTIVE = /\bTrustedUserCAKeys\b|\bHostCertificate\b|\bssh-keygen\s+-s\b/g;
89
+
90
+ const REMEDIATION =
91
+ "No post-quantum SSH certificate format is standardized yet — track OpenSSH release notes and IETF work on PQC signatures for SSH. SSH CA keys are long-lived trust roots, so plan for their rotation to a PQC signing algorithm (e.g. ML-DSA) as soon as a cert format lands, and keep validity periods short in the interim.";
92
+
93
+ const RULE_CA_RSA: RuleMeta = {
94
+ id: "ssh-ca-rsa-cert",
95
+ title: "SSH certificate authority — RSA signing",
96
+ description: "OpenSSH RSA certificate key type (ssh-rsa/rsa-sha2-*-cert-v01@openssh.com)",
97
+ category: "signature",
98
+ severity: "medium",
99
+ confidence: "high",
100
+ algorithm: "RSA",
101
+ hndl: false,
102
+ cwe: CWE_BROKEN_CRYPTO,
103
+ sensitive: true,
104
+ message:
105
+ "SSH certificate uses a classical RSA key type (ssh-rsa/rsa-sha2-*-cert-v01@openssh.com); the CA signature is forgeable once a CRQC can recover the RSA CA key.",
106
+ remediation: REMEDIATION,
107
+ };
108
+ const RULE_CA_ECDSA: RuleMeta = {
109
+ id: "ssh-ca-ecdsa-cert",
110
+ title: "SSH certificate authority — ECDSA signing",
111
+ description: "OpenSSH ECDSA certificate key type (ecdsa-sha2-nistp*-cert-v01@openssh.com)",
112
+ category: "signature",
113
+ severity: "medium",
114
+ confidence: "high",
115
+ algorithm: "ECDSA",
116
+ hndl: false,
117
+ cwe: CWE_BROKEN_CRYPTO,
118
+ sensitive: true,
119
+ message:
120
+ "SSH certificate uses a classical ECDSA key type (ecdsa-sha2-nistp*-cert-v01@openssh.com); the CA signature is forgeable once a CRQC can recover the ECDSA CA key.",
121
+ remediation: REMEDIATION,
122
+ };
123
+ const RULE_CA_EDDSA: RuleMeta = {
124
+ id: "ssh-ca-ed25519-cert",
125
+ title: "SSH certificate authority — EdDSA signing",
126
+ description: "OpenSSH Ed25519 certificate key type (ssh-ed25519-cert-v01@openssh.com)",
127
+ category: "signature",
128
+ severity: "medium",
129
+ confidence: "high",
130
+ algorithm: "EdDSA",
131
+ hndl: false,
132
+ cwe: CWE_BROKEN_CRYPTO,
133
+ sensitive: true,
134
+ message:
135
+ "SSH certificate uses a classical Ed25519 key type (ssh-ed25519-cert-v01@openssh.com); modern but still classical — the CA signature is forgeable once a CRQC can recover the Ed25519 CA key.",
136
+ remediation: REMEDIATION,
137
+ };
138
+
139
+ const RULE_CA_CONFIG: RuleMeta = {
140
+ id: "ssh-ca-config",
141
+ title: "SSH certificate authority configured",
142
+ description:
143
+ "OpenSSH CA deployment directive (TrustedUserCAKeys / HostCertificate / ssh-keygen -s)",
144
+ category: "signature",
145
+ severity: "medium",
146
+ // The directive names CA usage, not the algorithm — hence a lower confidence and
147
+ // an unknown family; the CA key file itself carries the specific algorithm.
148
+ confidence: "medium",
149
+ algorithm: "unknown",
150
+ hndl: false,
151
+ cwe: CWE_BROKEN_CRYPTO,
152
+ message:
153
+ "SSH certificate authority is configured (TrustedUserCAKeys / HostCertificate / ssh-keygen -s); the CA signing key is a long-lived classical trust root whose signatures become forgeable once a CRQC exists — verify the CA key's algorithm.",
154
+ remediation: REMEDIATION,
155
+ };
156
+
157
+ interface SshCaRule {
158
+ meta: RuleMeta;
159
+ re: RegExp;
160
+ }
161
+
162
+ const SSH_CA_RULES: readonly SshCaRule[] = [
163
+ { meta: RULE_CA_RSA, re: RE_CERT_RSA },
164
+ { meta: RULE_CA_ECDSA, re: RE_CERT_ECDSA },
165
+ { meta: RULE_CA_EDDSA, re: RE_CERT_EDDSA },
166
+ { meta: RULE_CA_CONFIG, re: RE_CA_DIRECTIVE },
167
+ ];
168
+
169
+ /** True when `content` carries some SSH-CA-specific marker (not just a bare token). */
170
+ function hasSshCaMarker(content: string): boolean {
171
+ return (
172
+ content.includes("cert-v01@openssh.com") ||
173
+ content.includes("@cert-authority") ||
174
+ content.includes("TrustedUserCAKeys") ||
175
+ content.includes("HostCertificate") ||
176
+ content.includes("ssh-keygen -s")
177
+ );
178
+ }
179
+
180
+ /**
181
+ * Detects classical SSH certificate-authority signing (OpenSSH cert key types)
182
+ * in SSH config, sshd_config, and known_hosts files. Distinct from SSH
183
+ * key-exchange (`ssh-kex-classical`): this is the certificate SIGNATURE surface.
184
+ */
185
+ export const sshCaDetector: Detector = {
186
+ id: "ssh-ca",
187
+ description: "Classical SSH certificate-authority signing (OpenSSH *-cert-v01@openssh.com)",
188
+ scope: "config",
189
+ language: "any",
190
+ rules: SSH_CA_RULES.map((r) => r.meta),
191
+ // Apply broadly (SSH CA config lives in many differently-named / extensionless
192
+ // files: sshd_config, known_hosts, *.pub) but never on prose/docs OR on program
193
+ // SOURCE files — a vendored SSH library or a constants table that spells out
194
+ // `ssh-ed25519-cert-v01@openssh.com` as a string literal is not a live CA config.
195
+ // The strict fast-reject in detect() is the remaining gate.
196
+ appliesTo: (f) =>
197
+ !hasExtension(f, DOC_EXTENSIONS) && !hasExtension(f, ANALYZABLE_SOURCE_EXTENSIONS),
198
+ detect({ file, content }): Finding[] {
199
+ if (!hasSshCaMarker(content)) return [];
200
+
201
+ // Mask `#` comment lines so a commented-out cert line can't fire; offsets are
202
+ // preserved. `@cert-authority` known_hosts lines start with `@`, not `#`, so
203
+ // they are intentionally left live.
204
+ const scan = maskCommentLines(content, ["#"]);
205
+ const findings: Finding[] = [];
206
+ for (const { meta, re } of SSH_CA_RULES) {
207
+ eachMatch(re, scan, (m) =>
208
+ findings.push(
209
+ findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
210
+ ),
211
+ );
212
+ }
213
+ return findings;
214
+ },
215
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Config detector: classical supply-chain / artifact signing beyond the cosign +
3
+ * GPG cases the `cicd` detector covers — Docker Content Trust (Notary v1 / TUF),
4
+ * CNCF Notation, and in-toto. These sign container images, OCI artifacts, and
5
+ * build provenance with classical keys (ECDSA / RSA / Ed25519). Like all signing,
6
+ * this is the signature-side analogue of harvest-now-decrypt-later: a signature
7
+ * produced today is forgeable once a CRQC exists, and the signed artifact keeps
8
+ * verifying against that classical key for years — so these are `hndl:false` but
9
+ * real quantum-migration debt.
10
+ *
11
+ * Gated to CI pipeline files, Dockerfiles, and shell scripts, over comment-masked
12
+ * content, and keyed on distinctive command/env tokens to keep false positives low.
13
+ */
14
+ import type { Detector, Finding, RuleMeta } from "../types.js";
15
+ import { eachMatch, findingFromRule, maskCommentLines } from "../detect-utils.js";
16
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
17
+
18
+ /** CI pipeline files, Dockerfiles, and shell scripts — where signing runs. */
19
+ function isSigningContext(filePath: string): boolean {
20
+ const lower = filePath.toLowerCase();
21
+ const base = lower.split("/").pop() ?? lower;
22
+ return (
23
+ (lower.includes(".github/workflows/") && (lower.endsWith(".yml") || lower.endsWith(".yaml"))) ||
24
+ base === ".gitlab-ci.yml" ||
25
+ lower.endsWith(".gitlab-ci.yml") ||
26
+ base === "jenkinsfile" ||
27
+ lower.endsWith(".jenkinsfile") ||
28
+ base === "dockerfile" ||
29
+ base.startsWith("dockerfile.") ||
30
+ lower.endsWith(".dockerfile") ||
31
+ lower.endsWith(".sh") ||
32
+ lower.endsWith(".bash")
33
+ );
34
+ }
35
+
36
+ interface ScRule {
37
+ re: RegExp;
38
+ meta: RuleMeta;
39
+ }
40
+
41
+ const SC_RULES: ScRule[] = [
42
+ {
43
+ re: /\bDOCKER_CONTENT_TRUST\s*[:=]\s*["']?1\b|\bdocker\s+trust\s+sign\b/g,
44
+ meta: {
45
+ id: "sc-docker-content-trust",
46
+ title: "Docker Content Trust signing (Notary v1)",
47
+ description: "Docker Content Trust / docker trust sign — Notary v1 (TUF) classical keys",
48
+ category: "signature",
49
+ severity: "medium",
50
+ confidence: "high",
51
+ algorithm: "ECDSA",
52
+ hndl: false,
53
+ cwe: CWE_BROKEN_CRYPTO,
54
+ message:
55
+ "Docker Content Trust signs images with Notary v1 (TUF) classical keys (ECDSA/Ed25519); signatures are forgeable once a CRQC exists.",
56
+ remediation: "Track sigstore/Notary v2 (Notation) PQC roadmap; plan hybrid image signing.",
57
+ },
58
+ },
59
+ {
60
+ re: /\bnotation\s+(?:sign|key\s+generate|cert\s+generate)\b/g,
61
+ meta: {
62
+ id: "sc-notation-sign",
63
+ title: "Notation artifact signing",
64
+ description: "CNCF Notation signing of OCI artifacts with classical keys",
65
+ category: "signature",
66
+ severity: "medium",
67
+ confidence: "high",
68
+ algorithm: "RSA",
69
+ hndl: false,
70
+ cwe: CWE_BROKEN_CRYPTO,
71
+ message:
72
+ "Notation signs OCI artifacts with a classical key (RSA/ECDSA); signatures are forgeable once a CRQC exists.",
73
+ remediation: "Track Notation's PQC signature support (ML-DSA) and plan migration.",
74
+ },
75
+ },
76
+ {
77
+ re: /\bin[-_]toto[-_]?run\b|\bin-toto\b/g,
78
+ meta: {
79
+ id: "sc-in-toto",
80
+ title: "in-toto supply-chain signing",
81
+ description: "in-toto build provenance signed with classical keys",
82
+ category: "signature",
83
+ severity: "medium",
84
+ confidence: "high",
85
+ algorithm: "RSA",
86
+ hndl: false,
87
+ cwe: CWE_BROKEN_CRYPTO,
88
+ message:
89
+ "in-toto signs build provenance with a classical key (RSA/Ed25519); the attestation is forgeable once a CRQC exists.",
90
+ remediation: "Track in-toto/DSSE PQC signature support and plan migration.",
91
+ },
92
+ },
93
+ ];
94
+
95
+ /** Detects classical Docker Content Trust / Notation / in-toto signing. */
96
+ export const supplyChainDetector: Detector = {
97
+ id: "supply-chain-signing",
98
+ description: "Classical container/artifact signing (Docker Content Trust, Notation, in-toto)",
99
+ scope: "config",
100
+ language: "any",
101
+ rules: SC_RULES.map((r) => r.meta),
102
+ appliesTo: isSigningContext,
103
+ detect({ file, content }): Finding[] {
104
+ // `#` covers YAML/Dockerfile/shell; `//` covers Jenkinsfile (Groovy), which has no
105
+ // extension so it is never centrally comment-stripped.
106
+ const scan = maskCommentLines(content, ["#", "//"]);
107
+ const findings: Finding[] = [];
108
+ for (const rule of SC_RULES) {
109
+ eachMatch(rule.re, scan, (m) => {
110
+ findings.push(
111
+ findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),
112
+ );
113
+ });
114
+ }
115
+ return findings;
116
+ },
117
+ };