@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
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Config/source detector: weak hash functions (SHA-1, MD5) used specifically in
3
+ * a DIGITAL-SIGNATURE or X.509 CERTIFICATE algorithm — NOT generic hashing and
4
+ * NOT password hashing.
5
+ *
6
+ * WHY THIS LIVES IN A PQC-READINESS TOOL (quantum-adjacent, not quantum-broken).
7
+ * SHA-1/MD5 are not "quantum-broken" — Grover only halves preimage strength and
8
+ * a CRQC does not conjure signature-hash collisions. We flag them anyway, and
9
+ * frame them as *quantum-adjacent: same migration window*:
10
+ * - NIST SP 800-131A Rev 3 disallows SHA-1 for generating digital signatures
11
+ * and retires SHA-1 entirely by 2030 — the SAME transition window the same
12
+ * document sets for the classical→PQC migration. One standard, one deadline.
13
+ * - CNSA 2.0 pairs its PQC mandate (ML-KEM / ML-DSA) with SHA-384/512; a
14
+ * CNSA-2.0 target that re-keys to PQC must also drop SHA-1 from its
15
+ * signature/certificate chain in the same effort.
16
+ * - A quantum-readiness migration RE-KEYS and RE-SIGNS every certificate and
17
+ * signature anyway. Surfacing weak signature hashes in that same pass costs
18
+ * nothing extra and closes a today-exploitable gap (SHA-1/MD5 signature
19
+ * forgery via chosen-prefix collisions) alongside the future one.
20
+ * We deliberately do NOT claim these hashes are broken by quantum computers.
21
+ *
22
+ * WHY THE SCOPE IS NARROW (signature/certificate ONLY).
23
+ * A bare `sha1(x)` or `sha1sum file` is often a perfectly acceptable
24
+ * non-security checksum (ETags, cache keys, content addressing, Git object ids).
25
+ * PASSWORD hashing with a weak/fast hash is a real weakness too, but it is a
26
+ * DIFFERENT weakness (CWE-916, needs a slow KDF like Argon2/bcrypt/scrypt), a
27
+ * different remediation, and — crucially — is NOT on the PQC migration path, so
28
+ * it does not belong in this detector. We only fire when the weak hash is bound
29
+ * to a signature/certificate algorithm, where "move to SHA-256+ while you re-key
30
+ * for PQC" is exactly the right, single remediation. That binding is enforced by
31
+ * a file-level fast-reject marker (see `hasSignatureMarker`) plus regexes that
32
+ * only match signature/cert *algorithm identifiers*, never a lone hash call.
33
+ *
34
+ * SURFACES COVERED (all verified signature/certificate contexts):
35
+ * 1. Java JCA standard names — `Signature.getInstance("SHA1withRSA")`,
36
+ * `"MD5withRSA"`, `"SHA1withDSA"`, `"SHA1withECDSA"` (JDK Standard Algorithm
37
+ * Names). Regex anchors on the `…with(RSA|DSA|ECDSA)` shape, so the token is
38
+ * unambiguously a signature algorithm, never a bare digest.
39
+ * 2. .NET signing calls — a weak hash passed to `.SignData(…)` / `.SignHash(…)`
40
+ * as `HashAlgorithmName.SHA1` or the legacy string `"SHA1"` (e.g.
41
+ * `RSACryptoServiceProvider.SignData(data, "SHA1")`). Matched only INSIDE the
42
+ * sign call, so an unrelated `HashAlgorithmName.SHA1` used for a checksum is
43
+ * not flagged.
44
+ * 3. X.509 / ASN.1 signature-algorithm identifiers — the OID *names*
45
+ * (`sha1WithRSAEncryption`, `md5WithRSAEncryption`, `ecdsa-with-SHA1`,
46
+ * `dsaWithSHA1`) and their dotted OIDs (`1.2.840.113549.1.1.5` =
47
+ * sha1WithRSAEncryption, `1.2.840.113549.1.1.4` = md5WithRSAEncryption,
48
+ * `1.2.840.10045.4.1` = ecdsa-with-SHA1). These are certificate
49
+ * signatureAlgorithm values by definition.
50
+ * 4. OpenSSL CLI in a signing/cert context — `-sha1` / `-md5` on the same line
51
+ * as `openssl` AND a certificate/signing subcommand or flag
52
+ * (`req` / `x509` / `ca` / `-sign` / `-signkey`). A bare `openssl dgst -sha1
53
+ * file` (a checksum, no `-sign`) is intentionally NOT matched.
54
+ *
55
+ * NOT DUPLICATED: XML-DSig `rsa-sha1` (see `xmldsig.ts`) and DKIM `rsa-sha1`
56
+ * (see `dkim.ts`) already emit SIGNATURE findings on their own surfaces; this
57
+ * detector deliberately does not touch those and focuses on the Java / .NET /
58
+ * X.509 / OpenSSL surfaces above.
59
+ *
60
+ * Category is `hash` (weak/pre-quantum hash usage — the previously-unused
61
+ * category). `hndl: false` — a signature has no confidentiality to harvest.
62
+ * `algorithm: "unknown"` — the weak *hash* is not itself an asymmetric family;
63
+ * the signature it weakens is (RSA/ECDSA/DSA), but the finding is about the hash.
64
+ */
65
+ import type { Detector, Finding, RuleMeta } from "../types.js";
66
+ import {
67
+ DOC_EXTENSIONS,
68
+ eachMatch,
69
+ findingFromRule,
70
+ hasExtension,
71
+ maskBlockComments,
72
+ maskCommentLines,
73
+ } from "../detect-utils.js";
74
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
75
+
76
+ // --- SHA-1 in a signature/certificate algorithm identifier ---------------------
77
+ // Java JCA standard names: `SHA1withRSA`, `SHA1withDSA`, `SHA1withECDSA`. The
78
+ // `…with(RSA|DSA|ECDSA)` shape makes the token unambiguously a signature alg.
79
+ // `-?` tolerates the `SHA-1` spelling; `i` covers case variation in real code.
80
+ const RE_SHA1_JAVA = /\bSHA-?1with(?:RSA|DSA|ECDSA)\b/gi;
81
+ // X.509 / ASN.1 signature-algorithm OID *names*.
82
+ const RE_SHA1_X509 = /\b(?:sha-?1WithRSAEncryption|ecdsa-with-SHA-?1|dsaWithSHA-?1)\b/gi;
83
+ // X.509 signature-algorithm dotted OIDs (sha1WithRSAEncryption, ecdsa-with-SHA1).
84
+ // Lookarounds keep the id from matching inside a longer OID.
85
+ const RE_SHA1_OID = /(?<![\d.])(?:1\.2\.840\.113549\.1\.1\.5|1\.2\.840\.10045\.4\.1)(?![\d.])/g;
86
+ // OpenSSL CLI: `-sha1` on the same line as `openssl` AND an actual SIGNING marker
87
+ // (`req` / `ca` subcommand, or `-sign`/`-signkey`/`-CA`/`-CAkey`), in either order.
88
+ // A bare `x509` subcommand is NOT a signing marker — `openssl x509 -fingerprint
89
+ // -sha1` / `-subject_hash` are read-only thumbprint/identifier ops, not signature
90
+ // generation (audit M1). `openssl dgst -sha1 file` (checksum) is likewise unmatched.
91
+ const OPENSSL_SIGN = String.raw`(?:\breq\b|\bca\b|-sign(?:key)?\b|-CA(?:key)?\b)`;
92
+ const RE_SHA1_OPENSSL = new RegExp(
93
+ `openssl\\b[^\\n]*?${OPENSSL_SIGN}[^\\n]*?-sha1\\b|openssl\\b[^\\n]*?-sha1\\b[^\\n]*?${OPENSSL_SIGN}`,
94
+ "gi",
95
+ );
96
+ // .NET: a weak hash bound to a signing call (`.SignData(…)` / `.SignHash(…)`), as
97
+ // `HashAlgorithmName.SHA1` or the legacy `"SHA1"` string arg. Bounded by `;` (the
98
+ // statement), NOT `)` — the first argument is often itself a call
99
+ // (`.SignData(Encoding.UTF8.GetBytes(m), HashAlgorithmName.SHA1, …)`), so a
100
+ // `[^)]` span would stop at the inner `)` and miss it (audit H3).
101
+ const RE_SHA1_DOTNET =
102
+ /\.Sign(?:Data|Hash)\s*\([^;]*?(?:HashAlgorithmName\.SHA-?1|["']SHA-?1["'])/gi;
103
+
104
+ // --- MD5 in a signature/certificate algorithm identifier -----------------------
105
+ const RE_MD5_JAVA = /\bMD5with(?:RSA|DSA|ECDSA)\b/gi;
106
+ const RE_MD5_X509 = /\bmd5WithRSAEncryption\b/gi;
107
+ const RE_MD5_OID = /(?<![\d.])1\.2\.840\.113549\.1\.1\.4(?![\d.])/g;
108
+ const RE_MD5_OPENSSL = new RegExp(
109
+ `openssl\\b[^\\n]*?${OPENSSL_SIGN}[^\\n]*?-md5\\b|openssl\\b[^\\n]*?-md5\\b[^\\n]*?${OPENSSL_SIGN}`,
110
+ "gi",
111
+ );
112
+ const RE_MD5_DOTNET = /\.Sign(?:Data|Hash)\s*\([^;]*?(?:HashAlgorithmName\.MD5|["']MD5["'])/gi;
113
+
114
+ const RULE_SHA1: RuleMeta = {
115
+ id: "weak-hash-sha1-signature",
116
+ title: "SHA-1 in a signature / certificate algorithm",
117
+ description:
118
+ "SHA-1 used in a digital-signature or X.509 certificate algorithm (Java/.NET/X.509/OpenSSL)",
119
+ category: "hash",
120
+ severity: "medium",
121
+ confidence: "high",
122
+ algorithm: "unknown",
123
+ hndl: false,
124
+ cwe: CWE_BROKEN_CRYPTO,
125
+ message:
126
+ "SHA-1 used in a digital-signature/certificate algorithm; SHA-1 is disallowed for signatures (NIST SP 800-131A Rev 3, fully retired 2030 — the same window as the PQC migration) and enables collision-based signature forgery. Quantum-adjacent: migrate the hash alongside the signature algorithm.",
127
+ remediation:
128
+ "Move signature/certificate hashes to SHA-256 or stronger (SHA-384/512 for CNSA 2.0 targets) and re-issue affected certificates; fold the PQC signature migration (ML-DSA) into the same re-key/re-sign effort.",
129
+ };
130
+ const RULE_MD5: RuleMeta = {
131
+ id: "weak-hash-md5-signature",
132
+ title: "MD5 in a signature / certificate algorithm",
133
+ description:
134
+ "MD5 used in a digital-signature or X.509 certificate algorithm (Java/.NET/X.509/OpenSSL)",
135
+ category: "hash",
136
+ severity: "high",
137
+ confidence: "high",
138
+ algorithm: "unknown",
139
+ hndl: false,
140
+ cwe: CWE_BROKEN_CRYPTO,
141
+ message:
142
+ "MD5 used in a digital-signature/certificate algorithm; MD5 signatures are catastrophically broken today (practical chosen-prefix collisions have forged CA certificates) and must be replaced immediately. Quantum-adjacent: migrate the hash alongside the signature algorithm in the PQC re-sign pass.",
143
+ remediation:
144
+ "Replace MD5 signature/certificate hashes with SHA-256 or stronger (SHA-384/512 for CNSA 2.0 targets) and re-issue affected certificates immediately; fold the PQC signature migration (ML-DSA) into the same re-key/re-sign effort.",
145
+ };
146
+
147
+ interface WeakHashRule {
148
+ meta: RuleMeta;
149
+ res: readonly RegExp[];
150
+ }
151
+
152
+ const WEAK_HASH_RULES: readonly WeakHashRule[] = [
153
+ {
154
+ meta: RULE_SHA1,
155
+ res: [RE_SHA1_JAVA, RE_SHA1_X509, RE_SHA1_OID, RE_SHA1_OPENSSL, RE_SHA1_DOTNET],
156
+ },
157
+ { meta: RULE_MD5, res: [RE_MD5_JAVA, RE_MD5_X509, RE_MD5_OID, RE_MD5_OPENSSL, RE_MD5_DOTNET] },
158
+ ];
159
+
160
+ /**
161
+ * True when `content` carries a signature/certificate marker — a signal that any
162
+ * weak hash present is bound to a signature or cert, not a bare checksum. This is
163
+ * the guard that keeps `sha1(x)` / `sha1sum` / a password hash from ever firing:
164
+ * without one of these markers the file is not scanned at all.
165
+ */
166
+ function hasSignatureMarker(content: string): boolean {
167
+ if (
168
+ /with\s?RSA|with\s?DSA|with\s?ECDSA|WithRSAEncryption|ecdsa-with-SHA-?1|dsaWithSHA-?1/i.test(
169
+ content,
170
+ )
171
+ ) {
172
+ return true;
173
+ }
174
+ if (/1\.2\.840\.113549\.1\.1\.[45]|1\.2\.840\.10045\.4\.1/.test(content)) return true;
175
+ if (/Signature\.getInstance|\.Sign(?:Data|Hash)\s*\(/.test(content)) return true;
176
+ // OpenSSL only counts as a marker with a cert/signing subcommand or flag.
177
+ if (/openssl/i.test(content) && /\b(?:req|x509|ca|-signkey|-sign|dgst)\b/i.test(content)) {
178
+ return true;
179
+ }
180
+ return false;
181
+ }
182
+
183
+ /**
184
+ * Detects SHA-1 / MD5 used specifically in signature or certificate algorithms
185
+ * (Java JCA, .NET signing calls, X.509 OID names/OIDs, OpenSSL cert/signing CLI).
186
+ */
187
+ export const weakHashDetector: Detector = {
188
+ id: "weak-hash-signature",
189
+ description: "Weak hash (SHA-1/MD5) in a digital-signature or X.509 certificate algorithm",
190
+ scope: "config",
191
+ language: "any",
192
+ rules: WEAK_HASH_RULES.map((r) => r.meta),
193
+ // Skip prose/docs: a page explaining `SHA1withRSA` is not live config/code.
194
+ appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
195
+ detect({ file, content }): Finding[] {
196
+ // Fast reject on the ORIGINAL content: only scan files whose weak hash is
197
+ // bound to a signature/cert context. A lone `sha1(...)` checksum or a
198
+ // password hash has no such marker and is skipped wholesale.
199
+ if (!hasSignatureMarker(content)) return [];
200
+
201
+ // Mask C-style block comments and `//` / `#` / `;` line comments so a
202
+ // commented-out algorithm can't fire. Offsets are preserved, so finding
203
+ // line/column/snippet for the live lines stay exact.
204
+ const scan = maskCommentLines(maskBlockComments(content), ["//", "#", ";"]);
205
+ const findings: Finding[] = [];
206
+ for (const { meta, res } of WEAK_HASH_RULES) {
207
+ for (const re of res) {
208
+ eachMatch(re, scan, (m) =>
209
+ findings.push(
210
+ findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
211
+ ),
212
+ );
213
+ }
214
+ }
215
+ return findings;
216
+ },
217
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Config detector: classical asymmetric signature algorithms pinned by
3
+ * WebAuthn / FIDO2 / passkey attestation & assertion, expressed as COSE
4
+ * algorithm identifiers in relying-party code and config
5
+ * (@simplewebauthn/server, py_webauthn, go-webauthn, webauthn4j).
6
+ *
7
+ * WHY WebAuthn is PQC-relevant: a WebAuthn credential is an asymmetric key
8
+ * pair whose PUBLIC key the authenticator returns (COSE-encoded) at
9
+ * registration; every subsequent assertion is a classical signature over the
10
+ * authentication challenge, and attestation statements are classical
11
+ * signatures over the credential. These are AUTHENTICATION signatures, not
12
+ * confidentiality — there is nothing to "harvest now" — but the moment a
13
+ * cryptographically-relevant quantum computer (CRQC) exists, an attacker who
14
+ * has a user's registered public key (or an attestation CA's key) can FORGE
15
+ * assertions/attestations and impersonate the user or a genuine authenticator.
16
+ * So every finding here is `category: "signature"`, `hndl: false`.
17
+ *
18
+ * The relying party pins which algorithms it will accept: `pubKeyCredParams`
19
+ * (registration) and `supportedAlgorithmIDs` (verification) enumerate COSE
20
+ * `alg` values, and the classical ones — ES256 (COSE -7, ECDSA P-256),
21
+ * RS256 (-257, RSA), EdDSA (-8) and their P-384/P-521/RSA-2048+ siblings — are
22
+ * exactly the ones a PQC migration must inventory and replace once FIDO/COSE
23
+ * standardise post-quantum algorithm identifiers.
24
+ *
25
+ * WHY numeric COSE ids need their own detector: the existing JWT/JOSE rule
26
+ * only matches QUOTED alg strings (`"ES256"`, `"RS256"`). WebAuthn libraries
27
+ * almost never write those strings — they use the NUMERIC COSE ids
28
+ * (`alg: -7`, `supportedAlgorithmIDs: [-7, -257]`) and language-level enum
29
+ * identifiers (`COSEAlgorithmIdentifier.ES256`, `webauthncose.AlgES256`,
30
+ * `COSEAlgorithmIdentifier.EDDSA`). None of those carry the quote characters
31
+ * the JWT rule keys off, so the entire WebAuthn surface is invisible today.
32
+ *
33
+ * Two independent match shapes per family, both gated by a file-level WebAuthn
34
+ * marker (see `hasWebauthnMarker`) so the generic numeric `alg: -7` form can't
35
+ * fire on unrelated code that happens to assign a small negative number:
36
+ *
37
+ * 1. NUMERIC form — the COSE `alg` value as an integer, either as an object
38
+ * member (`alg: -7`, `alg = -257`) or inside a `supportedAlgorithmIDs`
39
+ * array (`supportedAlgorithmIDs: [-7, -257]`). Restricted to the classical
40
+ * COSE numbers only; symmetric / KDF / "direct" ids (e.g. -6, HS*) are
41
+ * never in the alternation, so they cannot match.
42
+ * 2. ENUM form — the COSE algorithm's identifier as written in each library:
43
+ * `COSEAlgorithmIdentifier.ES256|RS256|EdDSA|ECDSA_SHA_256` (webauthn4j,
44
+ * py_webauthn) and Go's `webauthncose.AlgES256` / `AlgRS256` / `AlgEdDSA`
45
+ * constants.
46
+ *
47
+ * COSE algorithm numbers → {@link AlgorithmFamily} (per the COSE registry, using
48
+ * the mapping this detector is specified against):
49
+ * - ES256 (-7), ES384 (-35), ES512 (-36) → ECDSA
50
+ * - RS256 (-257), RS384 (-258), RS512 (-259) → RSA
51
+ * - EdDSA (-8) → EdDSA
52
+ *
53
+ * Fast reject: `detect()` bails unless the file carries a distinctive WebAuthn
54
+ * API token (`pubKeyCredParams`, `PublicKeyCredential`, `webauthn`,
55
+ * `COSEAlgorithm`, `supportedAlgorithmIDs`, `navigator.credentials`). Those
56
+ * tokens are the marker gate ONLY — they never produce a standalone finding.
57
+ */
58
+ import type { Detector, Finding, RuleMeta } from "../types.js";
59
+ import {
60
+ DOC_EXTENSIONS,
61
+ eachMatch,
62
+ findingFromRule,
63
+ hasExtension,
64
+ maskBlockComments,
65
+ maskCommentLines,
66
+ } from "../detect-utils.js";
67
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
68
+
69
+ // Classical COSE algorithm numbers, grouped by family (see module docstring).
70
+ const NUM_ECDSA = "7|35|36"; // ES256, ES384, ES512
71
+ const NUM_RSA = "257|258|259"; // RS256, RS384, RS512
72
+ const NUM_EDDSA = "8"; // EdDSA
73
+
74
+ // --- NUMERIC form: a COSE `alg` value written as an integer. ---
75
+ // (a) object member: `alg: -7`, `alg = -257` (JS/JSON, Python `alg=-7`).
76
+ // `["']?alg["']?` tolerates the JSON/serialized form `"alg": -7` (the WebAuthn
77
+ // options object is JSON-serializable and often stored/transmitted/tested as JSON
78
+ // with a quoted key), as well as the unquoted `alg: -7` / `alg = -7` (audit M2).
79
+ const RE_NUM_ECDSA = new RegExp(`["']?\\balg\\b["']?\\s*[:=]\\s*-(?:${NUM_ECDSA})\\b`, "g");
80
+ const RE_NUM_RSA = new RegExp(`["']?\\balg\\b["']?\\s*[:=]\\s*-(?:${NUM_RSA})\\b`, "g");
81
+ const RE_NUM_EDDSA = new RegExp(`["']?\\balg\\b["']?\\s*[:=]\\s*-(?:${NUM_EDDSA})\\b`, "g");
82
+ // (b) `supportedAlgorithmIDs: [-7, -257]` array (@simplewebauthn verify option).
83
+ // `[^\]]*` stays inside a single array literal; one match per family even when
84
+ // the array lists several ids of that family.
85
+ const RE_IDS_ECDSA = new RegExp(
86
+ `supportedAlgorithmIDs\\s*[:=]\\s*\\[[^\\]]*-(?:${NUM_ECDSA})\\b`,
87
+ "g",
88
+ );
89
+ const RE_IDS_RSA = new RegExp(`supportedAlgorithmIDs\\s*[:=]\\s*\\[[^\\]]*-(?:${NUM_RSA})\\b`, "g");
90
+ const RE_IDS_EDDSA = new RegExp(
91
+ `supportedAlgorithmIDs\\s*[:=]\\s*\\[[^\\]]*-(?:${NUM_EDDSA})\\b`,
92
+ "g",
93
+ );
94
+
95
+ // --- ENUM form: the COSE algorithm's language-level identifier. ---
96
+ // webauthn4j / py_webauthn: `COSEAlgorithmIdentifier.<name>`; go-webauthn:
97
+ // `webauthncose.Alg<name>` (matched bare as `Alg<name>`).
98
+ const RE_ENUM_ECDSA =
99
+ /\bCOSEAlgorithmIdentifier\.(?:ES(?:256|384|512)|ECDSA_SHA_(?:256|384|512))\b|\bAlgES(?:256|384|512)\b/g;
100
+ const RE_ENUM_RSA =
101
+ /\bCOSEAlgorithmIdentifier\.(?:RS(?:256|384|512)|RSASSA_[A-Za-z0-9_]+)\b|\bAlgRS(?:256|384|512)\b/g;
102
+ const RE_ENUM_EDDSA = /\bCOSEAlgorithmIdentifier\.(?:EdDSA|EDDSA)\b|\bAlgEdDSA\b/g;
103
+
104
+ const REMEDIATION =
105
+ "WebAuthn/FIDO2 is standardizing PQC COSE algorithms; inventory the classical attestation/assertion algs now and plan migration.";
106
+
107
+ const RULE_WEBAUTHN_ECDSA: RuleMeta = {
108
+ id: "webauthn-ecdsa",
109
+ title: "WebAuthn ECDSA COSE algorithm",
110
+ description:
111
+ "WebAuthn/FIDO2 credential pins a classical ECDSA COSE algorithm (ES256/ES384/ES512)",
112
+ category: "signature",
113
+ severity: "medium",
114
+ confidence: "high",
115
+ algorithm: "ECDSA",
116
+ hndl: false,
117
+ cwe: CWE_BROKEN_CRYPTO,
118
+ message:
119
+ "WebAuthn/FIDO2 relying party accepts a classical ECDSA COSE algorithm (ES256/ES384/ES512); attestation/assertion signatures become forgeable once a CRQC exists.",
120
+ remediation: REMEDIATION,
121
+ };
122
+ const RULE_WEBAUTHN_RSA: RuleMeta = {
123
+ id: "webauthn-rsa",
124
+ title: "WebAuthn RSA COSE algorithm",
125
+ description: "WebAuthn/FIDO2 credential pins a classical RSA COSE algorithm (RS256/RS384/RS512)",
126
+ category: "signature",
127
+ severity: "medium",
128
+ confidence: "high",
129
+ algorithm: "RSA",
130
+ hndl: false,
131
+ cwe: CWE_BROKEN_CRYPTO,
132
+ message:
133
+ "WebAuthn/FIDO2 relying party accepts a classical RSA COSE algorithm (RS256/RS384/RS512); attestation/assertion signatures become forgeable once a CRQC exists.",
134
+ remediation: REMEDIATION,
135
+ };
136
+ const RULE_WEBAUTHN_EDDSA: RuleMeta = {
137
+ id: "webauthn-eddsa",
138
+ title: "WebAuthn EdDSA COSE algorithm",
139
+ description: "WebAuthn/FIDO2 credential pins the classical EdDSA COSE algorithm (COSE -8)",
140
+ category: "signature",
141
+ severity: "medium",
142
+ confidence: "high",
143
+ algorithm: "EdDSA",
144
+ hndl: false,
145
+ cwe: CWE_BROKEN_CRYPTO,
146
+ message:
147
+ "WebAuthn/FIDO2 relying party accepts the classical EdDSA COSE algorithm (Ed25519); modern but still classical — attestation/assertion signatures become forgeable once a CRQC exists.",
148
+ remediation: REMEDIATION,
149
+ };
150
+
151
+ interface WebauthnRule {
152
+ meta: RuleMeta;
153
+ res: readonly RegExp[];
154
+ }
155
+
156
+ const WEBAUTHN_RULES: readonly WebauthnRule[] = [
157
+ { meta: RULE_WEBAUTHN_ECDSA, res: [RE_NUM_ECDSA, RE_IDS_ECDSA, RE_ENUM_ECDSA] },
158
+ { meta: RULE_WEBAUTHN_RSA, res: [RE_NUM_RSA, RE_IDS_RSA, RE_ENUM_RSA] },
159
+ { meta: RULE_WEBAUTHN_EDDSA, res: [RE_NUM_EDDSA, RE_IDS_EDDSA, RE_ENUM_EDDSA] },
160
+ ];
161
+
162
+ /**
163
+ * True when `content` carries a distinctive WebAuthn API token. Used ONLY as
164
+ * the fast-reject marker gate — none of these tokens produce a finding — so the
165
+ * generic numeric `alg: -7` form can't fire on unrelated code.
166
+ */
167
+ function hasWebauthnMarker(content: string): boolean {
168
+ return (
169
+ content.includes("pubKeyCredParams") ||
170
+ content.includes("PublicKeyCredential") ||
171
+ content.includes("COSEAlgorithm") ||
172
+ content.includes("supportedAlgorithmIDs") ||
173
+ content.includes("navigator.credentials") ||
174
+ /webauthn/i.test(content)
175
+ );
176
+ }
177
+
178
+ /** Detects classical COSE signature algorithms pinned by WebAuthn/FIDO2 relying parties. */
179
+ export const webauthnDetector: Detector = {
180
+ id: "webauthn-crypto",
181
+ description: "Classical COSE signature algorithms in WebAuthn/FIDO2/passkey relying-party code",
182
+ scope: "config",
183
+ language: "any",
184
+ rules: WEBAUTHN_RULES.map((r) => r.meta),
185
+ appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
186
+ detect({ file, content }): Finding[] {
187
+ if (!hasWebauthnMarker(content)) return [];
188
+
189
+ // Mask C-style (`//`, `/* */`) and hash (`#`) comments so a commented-out
190
+ // options object can't fire. Offsets are preserved so finding locations stay
191
+ // exact.
192
+ const scan = maskCommentLines(maskBlockComments(content), ["//", "#"]);
193
+ const findings: Finding[] = [];
194
+ for (const { meta, res } of WEBAUTHN_RULES) {
195
+ for (const re of res) {
196
+ eachMatch(re, scan, (m) =>
197
+ findings.push(
198
+ findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
199
+ ),
200
+ );
201
+ }
202
+ }
203
+ return findings;
204
+ },
205
+ };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Config detector: classical crypto in XML Digital Signature / XML Encryption
3
+ * (W3C XML-DSig / XML-Enc), the algorithm layer under SAML SSO, WS-Security /
4
+ * SOAP, and signed XML documents. Enterprise identity is ubiquitous and its IdP
5
+ * signing keys are long-lived, so a forged SAML assertion is a prime quantum
6
+ * forgery surface; an encrypted SAML assertion (RSA key transport) is
7
+ * harvest-now-decrypt-later exposed.
8
+ *
9
+ * Detection keys off the W3C algorithm URIs, which are globally-unique constant
10
+ * strings (RFC-precise, so a lexical match is exact):
11
+ * - `…xmldsig#rsa-sha1` / `…xmldsig-more#rsa-sha256` … → RSA signature (forgeable)
12
+ * - `…xmldsig11#dsa-sha256` / `…xmldsig#dsa-sha1` → DSA signature
13
+ * - `…xmldsig-more#ecdsa-sha256` … → ECDSA signature
14
+ * - `…xmlenc#rsa-oaep(-mgf1p)` / `…xmlenc#rsa-1_5` → RSA key transport (HNDL)
15
+ *
16
+ * These URIs appear both in XML config (`.xml`) and as string constants in SAML
17
+ * library code (python3-saml, xml-crypto, OpenSAML), so the detector runs on any
18
+ * non-doc file.
19
+ */
20
+ import type { Detector, Finding, RuleMeta } from "../types.js";
21
+ import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
22
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
23
+
24
+ // The `#fragment` of the W3C XML-DSig / XML-Enc algorithm URIs. Anchored on the
25
+ // namespace fragment (`xmldsig…#` / `xmlenc#`) so a bare `rsa-sha256` elsewhere
26
+ // doesn't misfire.
27
+ const RE_XMLDSIG_RSA = /\bxmldsig(?:-more|11)?#rsa-sha(?:1|224|256|384|512)\b/g;
28
+ const RE_XMLDSIG_DSA = /\bxmldsig(?:11)?#dsa-sha(?:1|256)\b/g;
29
+ const RE_XMLDSIG_ECDSA = /\bxmldsig-more#ecdsa-sha(?:1|224|256|384|512)\b/g;
30
+ const RE_XMLENC_RSA = /\bxmlenc#rsa-(?:oaep(?:-mgf1p)?|1_5)\b/g;
31
+
32
+ const RULE_XMLDSIG_RSA: RuleMeta = {
33
+ id: "xmldsig-rsa-sign",
34
+ title: "XML-DSig RSA signature",
35
+ description: "XML Digital Signature with an RSA-SHA* algorithm (SAML/WS-Security)",
36
+ category: "signature",
37
+ severity: "medium",
38
+ confidence: "high",
39
+ algorithm: "RSA",
40
+ hndl: false,
41
+ cwe: CWE_BROKEN_CRYPTO,
42
+ message:
43
+ "XML signatures (SAML / WS-Security) use classical RSA (rsa-sha*); a quantum attacker could forge assertions signed with this key.",
44
+ remediation:
45
+ "Track PQC XML-DSig / SAML profiles (ML-DSA); rotate to a PQC signing key as tooling and IdPs add support.",
46
+ };
47
+ const RULE_XMLDSIG_DSA: RuleMeta = {
48
+ id: "xmldsig-dsa-sign",
49
+ title: "XML-DSig DSA signature",
50
+ description: "XML Digital Signature with a DSA-SHA* algorithm",
51
+ category: "signature",
52
+ severity: "medium",
53
+ confidence: "high",
54
+ algorithm: "DSA",
55
+ hndl: false,
56
+ cwe: CWE_BROKEN_CRYPTO,
57
+ message: "XML signatures use classical DSA (dsa-sha*) — deprecated and quantum-forgeable.",
58
+ remediation: "Migrate off DSA now; track PQC XML-DSig profiles (ML-DSA).",
59
+ };
60
+ const RULE_XMLDSIG_ECDSA: RuleMeta = {
61
+ id: "xmldsig-ecdsa-sign",
62
+ title: "XML-DSig ECDSA signature",
63
+ description: "XML Digital Signature with an ECDSA-SHA* algorithm",
64
+ category: "signature",
65
+ severity: "medium",
66
+ confidence: "high",
67
+ algorithm: "ECDSA",
68
+ hndl: false,
69
+ cwe: CWE_BROKEN_CRYPTO,
70
+ message: "XML signatures use classical ECDSA (ecdsa-sha*); forgeable by a quantum attacker.",
71
+ remediation:
72
+ "Track PQC XML-DSig / SAML profiles (ML-DSA); rotate the signing key when supported.",
73
+ };
74
+ const RULE_XMLENC_RSA: RuleMeta = {
75
+ id: "xmlenc-rsa-keytransport",
76
+ title: "XML-Enc RSA key transport",
77
+ description: "XML Encryption with RSA-OAEP / RSA-1_5 key transport (encrypted SAML assertions)",
78
+ category: "kem",
79
+ severity: "high",
80
+ confidence: "high",
81
+ algorithm: "RSA",
82
+ hndl: true,
83
+ cwe: CWE_BROKEN_CRYPTO,
84
+ message:
85
+ "XML Encryption wraps the content key with classical RSA (rsa-oaep / rsa-1_5); encrypted XML (e.g. SAML assertions) is harvest-now-decrypt-later exposed.",
86
+ remediation:
87
+ "Plan migration to a post-quantum KEM (ML-KEM-768) for key transport as XML-Enc / SAML PQC profiles mature.",
88
+ };
89
+
90
+ /** Detects classical XML-DSig / XML-Enc algorithms (SAML, WS-Security, signed XML). */
91
+ export const xmldsigDetector: Detector = {
92
+ id: "xmldsig-crypto",
93
+ description: "Classical XML-DSig / XML-Enc algorithms (SAML, WS-Security, signed XML)",
94
+ scope: "config",
95
+ language: "any",
96
+ // Skip prose/docs: a page explaining the SAML algorithm URIs is not live config.
97
+ appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
98
+ rules: [RULE_XMLDSIG_RSA, RULE_XMLDSIG_DSA, RULE_XMLDSIG_ECDSA, RULE_XMLENC_RSA],
99
+ detect({ file, content }): Finding[] {
100
+ // Fast reject: only proceed if an XML-DSig / XML-Enc namespace fragment is present.
101
+ if (!content.includes("xmldsig") && !content.includes("xmlenc")) return [];
102
+ const findings: Finding[] = [];
103
+ const add = (re: RegExp, rule: RuleMeta): void =>
104
+ eachMatch(re, content, (m) =>
105
+ findings.push(
106
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
107
+ ),
108
+ );
109
+ add(RE_XMLDSIG_RSA, RULE_XMLDSIG_RSA);
110
+ add(RE_XMLDSIG_DSA, RULE_XMLDSIG_DSA);
111
+ add(RE_XMLDSIG_ECDSA, RULE_XMLDSIG_ECDSA);
112
+ add(RE_XMLENC_RSA, RULE_XMLENC_RSA);
113
+ return findings;
114
+ },
115
+ };
package/src/evidence.ts CHANGED
@@ -52,10 +52,21 @@ export interface ReadinessReport {
52
52
  attestation: {
53
53
  /** sha256 over the canonicalized deterministic body (excludes scanTimeUtc). */
54
54
  contentHash: string;
55
- /** RFC-3161 / transparency-log token — filled by an external signer. */
56
- timestamp: null;
57
- /** Detached signature over `contentHash` filled by an external signer. */
58
- signature: null;
55
+ /**
56
+ * RFC-3161 / transparency-log token over `contentHash`, produced by an EXTERNAL
57
+ * timestamper (opaque string, e.g. base64). `null` until {@link signReadinessReport}
58
+ * runs one.
59
+ */
60
+ timestamp: string | null;
61
+ /**
62
+ * Detached signature over `contentHash`, produced by an EXTERNAL signer (opaque
63
+ * string, e.g. base64/PEM). `null` until {@link signReadinessReport} runs one.
64
+ */
65
+ signature: string | null;
66
+ /** Non-sensitive provenance label of the signer (e.g. "openssl", "cosign"). */
67
+ signedWith?: string;
68
+ /** Non-sensitive provenance label of the timestamper (e.g. "openssl-ts"). */
69
+ timestampedWith?: string;
59
70
  };
60
71
  }
61
72
 
@@ -133,3 +144,115 @@ export function buildReadinessReport(
133
144
  attestation: { contentHash, timestamp: null, signature: null },
134
145
  } as ReadinessReport;
135
146
  }
147
+
148
+ /** The result of {@link verifyReadinessReport}. */
149
+ export interface VerifyReadinessResult {
150
+ /** True iff the recomputed body hash equals the hash claimed in the attestation. */
151
+ valid: boolean;
152
+ /** The hash recomputed over the report's CURRENT body. */
153
+ computedHash: string;
154
+ /** The hash claimed in the report's attestation (`attestation.contentHash`). */
155
+ claimedHash: string;
156
+ /** A short human reason; present only when `valid` is false. */
157
+ reason?: string;
158
+ }
159
+
160
+ /**
161
+ * Recompute the deterministic content hash over a readiness report's body and
162
+ * compare it to the hash the attestation claims. Detects tampering with ANY
163
+ * hashed field — a finding, the inventory, a policy verdict, or subject/tool
164
+ * metadata: editing it after the fact changes the recomputed hash, so `valid`
165
+ * becomes false.
166
+ *
167
+ * By construction the scan timestamp, the CBOM envelope, and the attestation
168
+ * block itself are EXCLUDED from the hash (see {@link buildReadinessReport}), so
169
+ * touching those does not fail verification — their integrity follows from their
170
+ * hashed inputs. The body is reconstructed from the report's OWN stored fields
171
+ * (including `tool.version`), so a report built by an older qScan still verifies.
172
+ *
173
+ * This checks the INTEGRITY hash only. It does NOT validate the detached
174
+ * signature or RFC-3161 timestamp: those are opaque tokens from an external
175
+ * signer (ADR-0004) and are verified with that signer's own tooling.
176
+ */
177
+ export function verifyReadinessReport(report: ReadinessReport): VerifyReadinessResult {
178
+ const hashableBody = {
179
+ reportType: report.reportType,
180
+ specVersion: report.specVersion,
181
+ subject: {
182
+ repository: report.subject.repository,
183
+ commit: report.subject.commit,
184
+ scannedRoot: report.subject.scannedRoot,
185
+ },
186
+ tool: { name: report.tool.name, version: report.tool.version },
187
+ inventory: report.inventory,
188
+ findings: report.findings,
189
+ ...(report.policyMapping ? { policyMapping: report.policyMapping } : {}),
190
+ };
191
+ const computedHash =
192
+ "sha256:" +
193
+ createHash("sha256")
194
+ .update(JSON.stringify(canonicalize(hashableBody)))
195
+ .digest("hex");
196
+ const claimedHash = report.attestation.contentHash;
197
+ if (computedHash === claimedHash) {
198
+ return { valid: true, computedHash, claimedHash };
199
+ }
200
+ return {
201
+ valid: false,
202
+ computedHash,
203
+ claimedHash,
204
+ reason: "content-hash mismatch: the report body was modified after it was built",
205
+ };
206
+ }
207
+
208
+ /**
209
+ * An EXTERNAL signer/timestamper the tool orchestrates. Per ADR-0004 the tool
210
+ * implements no cryptography: it hands the payload to an operator-provided signer
211
+ * (an `openssl`/`cosign` invocation, an RFC-3161 TSA client, …) and records what
212
+ * comes back. `label` is a short, non-sensitive provenance string (e.g. the signer
213
+ * program name) — NOT the full command, which may contain a key path.
214
+ */
215
+ export interface EvidenceSigner {
216
+ label: string;
217
+ /**
218
+ * Produce a detached signature / timestamp token (opaque string) over `payload`.
219
+ * May be async so a future signer can shell out OR call a KMS / RFC-3161 TSA over
220
+ * the network without foreclosing that once this contract freezes at 1.0.
221
+ */
222
+ sign(payload: string): string | Promise<string>;
223
+ }
224
+
225
+ /** Options for {@link signReadinessReport}: a detached-signature and/or a timestamp signer. */
226
+ export interface SignEvidenceOptions {
227
+ signer?: EvidenceSigner;
228
+ timestamper?: EvidenceSigner;
229
+ }
230
+
231
+ /**
232
+ * Fill a readiness report's attestation with a detached signature and/or RFC-3161
233
+ * timestamp, produced by EXTERNAL signers over the report's `contentHash`. Pure
234
+ * orchestration: it invokes the injected signers and records their opaque output
235
+ * plus a provenance label — it performs no cryptography itself (ADR-0004). Returns a
236
+ * NEW report; the hashed body is untouched (attestation is excluded from the hash),
237
+ * so signing never changes `contentHash`.
238
+ */
239
+ export async function signReadinessReport(
240
+ report: ReadinessReport,
241
+ opts: SignEvidenceOptions,
242
+ ): Promise<ReadinessReport> {
243
+ const payload = report.attestation.contentHash;
244
+ const signature = opts.signer ? await opts.signer.sign(payload) : report.attestation.signature;
245
+ const timestamp = opts.timestamper
246
+ ? await opts.timestamper.sign(payload)
247
+ : report.attestation.timestamp;
248
+ return {
249
+ ...report,
250
+ attestation: {
251
+ ...report.attestation,
252
+ signature,
253
+ timestamp,
254
+ ...(opts.signer ? { signedWith: opts.signer.label } : {}),
255
+ ...(opts.timestamper ? { timestampedWith: opts.timestamper.label } : {}),
256
+ },
257
+ };
258
+ }