@quantakrypto/core 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +38 -10
  3. package/dist/agent-types.js.map +1 -1
  4. package/dist/baseline.js.map +1 -1
  5. package/dist/cache.js.map +1 -1
  6. package/dist/cbom-merge.d.ts +9 -0
  7. package/dist/cbom-merge.d.ts.map +1 -0
  8. package/dist/cbom-merge.js +99 -0
  9. package/dist/cbom-merge.js.map +1 -0
  10. package/dist/cbom.js.map +1 -1
  11. package/dist/changed.js.map +1 -1
  12. package/dist/codemods/config-toggle.js.map +1 -1
  13. package/dist/codemods/registry.js.map +1 -1
  14. package/dist/comments.d.ts +29 -2
  15. package/dist/comments.d.ts.map +1 -1
  16. package/dist/comments.js +178 -18
  17. package/dist/comments.js.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/cwe.js.map +1 -1
  20. package/dist/dependencies.d.ts +3 -2
  21. package/dist/dependencies.d.ts.map +1 -1
  22. package/dist/dependencies.js +138 -2
  23. package/dist/dependencies.js.map +1 -1
  24. package/dist/detect-utils.d.ts +19 -4
  25. package/dist/detect-utils.d.ts.map +1 -1
  26. package/dist/detect-utils.js +43 -6
  27. package/dist/detect-utils.js.map +1 -1
  28. package/dist/detectors/c.d.ts +4 -2
  29. package/dist/detectors/c.d.ts.map +1 -1
  30. package/dist/detectors/c.js +382 -3
  31. package/dist/detectors/c.js.map +1 -1
  32. package/dist/detectors/cicd.d.ts +27 -0
  33. package/dist/detectors/cicd.d.ts.map +1 -0
  34. package/dist/detectors/cicd.js +116 -0
  35. package/dist/detectors/cicd.js.map +1 -0
  36. package/dist/detectors/cloud-kms.d.ts +21 -0
  37. package/dist/detectors/cloud-kms.d.ts.map +1 -0
  38. package/dist/detectors/cloud-kms.js +57 -0
  39. package/dist/detectors/cloud-kms.js.map +1 -0
  40. package/dist/detectors/cloudformation.d.ts +30 -0
  41. package/dist/detectors/cloudformation.d.ts.map +1 -0
  42. package/dist/detectors/cloudformation.js +170 -0
  43. package/dist/detectors/cloudformation.js.map +1 -0
  44. package/dist/detectors/csharp.d.ts.map +1 -1
  45. package/dist/detectors/csharp.js +141 -3
  46. package/dist/detectors/csharp.js.map +1 -1
  47. package/dist/detectors/database.d.ts +17 -0
  48. package/dist/detectors/database.d.ts.map +1 -0
  49. package/dist/detectors/database.js +60 -0
  50. package/dist/detectors/database.js.map +1 -0
  51. package/dist/detectors/dnssec.d.ts +55 -0
  52. package/dist/detectors/dnssec.d.ts.map +1 -0
  53. package/dist/detectors/dnssec.js +107 -0
  54. package/dist/detectors/dnssec.js.map +1 -0
  55. package/dist/detectors/elixir.d.ts +20 -0
  56. package/dist/detectors/elixir.d.ts.map +1 -0
  57. package/dist/detectors/elixir.js +195 -0
  58. package/dist/detectors/elixir.js.map +1 -0
  59. package/dist/detectors/go.d.ts +5 -3
  60. package/dist/detectors/go.d.ts.map +1 -1
  61. package/dist/detectors/go.js +187 -3
  62. package/dist/detectors/go.js.map +1 -1
  63. package/dist/detectors/java.d.ts.map +1 -1
  64. package/dist/detectors/java.js +169 -4
  65. package/dist/detectors/java.js.map +1 -1
  66. package/dist/detectors/jose.d.ts +20 -0
  67. package/dist/detectors/jose.d.ts.map +1 -0
  68. package/dist/detectors/jose.js +62 -0
  69. package/dist/detectors/jose.js.map +1 -0
  70. package/dist/detectors/jwk.d.ts +25 -0
  71. package/dist/detectors/jwk.d.ts.map +1 -0
  72. package/dist/detectors/jwk.js +95 -0
  73. package/dist/detectors/jwk.js.map +1 -0
  74. package/dist/detectors/k8s.d.ts +17 -0
  75. package/dist/detectors/k8s.d.ts.map +1 -0
  76. package/dist/detectors/k8s.js +85 -0
  77. package/dist/detectors/k8s.js.map +1 -0
  78. package/dist/detectors/mesh.d.ts +35 -0
  79. package/dist/detectors/mesh.d.ts.map +1 -0
  80. package/dist/detectors/mesh.js +109 -0
  81. package/dist/detectors/mesh.js.map +1 -0
  82. package/dist/detectors/messaging.d.ts +16 -0
  83. package/dist/detectors/messaging.d.ts.map +1 -0
  84. package/dist/detectors/messaging.js +72 -0
  85. package/dist/detectors/messaging.js.map +1 -0
  86. package/dist/detectors/pem.d.ts.map +1 -1
  87. package/dist/detectors/pem.js +50 -2
  88. package/dist/detectors/pem.js.map +1 -1
  89. package/dist/detectors/php.d.ts +24 -0
  90. package/dist/detectors/php.d.ts.map +1 -0
  91. package/dist/detectors/php.js +167 -0
  92. package/dist/detectors/php.js.map +1 -0
  93. package/dist/detectors/python.d.ts.map +1 -1
  94. package/dist/detectors/python.js +144 -3
  95. package/dist/detectors/python.js.map +1 -1
  96. package/dist/detectors/ruby.d.ts +9 -5
  97. package/dist/detectors/ruby.d.ts.map +1 -1
  98. package/dist/detectors/ruby.js +122 -2
  99. package/dist/detectors/ruby.js.map +1 -1
  100. package/dist/detectors/rust.d.ts +8 -3
  101. package/dist/detectors/rust.d.ts.map +1 -1
  102. package/dist/detectors/rust.js +253 -3
  103. package/dist/detectors/rust.js.map +1 -1
  104. package/dist/detectors/secrets.d.ts +21 -0
  105. package/dist/detectors/secrets.d.ts.map +1 -0
  106. package/dist/detectors/secrets.js +79 -0
  107. package/dist/detectors/secrets.js.map +1 -0
  108. package/dist/detectors/source.d.ts.map +1 -1
  109. package/dist/detectors/source.js +305 -52
  110. package/dist/detectors/source.js.map +1 -1
  111. package/dist/detectors/stateful-hbs.d.ts +22 -0
  112. package/dist/detectors/stateful-hbs.d.ts.map +1 -0
  113. package/dist/detectors/stateful-hbs.js +142 -0
  114. package/dist/detectors/stateful-hbs.js.map +1 -0
  115. package/dist/detectors/terraform.d.ts +21 -0
  116. package/dist/detectors/terraform.d.ts.map +1 -0
  117. package/dist/detectors/terraform.js +123 -0
  118. package/dist/detectors/terraform.js.map +1 -0
  119. package/dist/detectors/vpn.d.ts +22 -0
  120. package/dist/detectors/vpn.d.ts.map +1 -0
  121. package/dist/detectors/vpn.js +99 -0
  122. package/dist/detectors/vpn.js.map +1 -0
  123. package/dist/errors.js.map +1 -1
  124. package/dist/evidence.d.ts +53 -0
  125. package/dist/evidence.d.ts.map +1 -0
  126. package/dist/evidence.js +81 -0
  127. package/dist/evidence.js.map +1 -0
  128. package/dist/index.d.ts +9 -2
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +10 -2
  131. package/dist/index.js.map +1 -1
  132. package/dist/inventory.d.ts +2 -0
  133. package/dist/inventory.d.ts.map +1 -1
  134. package/dist/inventory.js +27 -1
  135. package/dist/inventory.js.map +1 -1
  136. package/dist/parallel.js.map +1 -1
  137. package/dist/patch-policy.js.map +1 -1
  138. package/dist/policy.d.ts +67 -0
  139. package/dist/policy.d.ts.map +1 -0
  140. package/dist/policy.js +115 -0
  141. package/dist/policy.js.map +1 -0
  142. package/dist/redact.d.ts.map +1 -1
  143. package/dist/redact.js +39 -0
  144. package/dist/redact.js.map +1 -1
  145. package/dist/registry.d.ts +2 -2
  146. package/dist/registry.d.ts.map +1 -1
  147. package/dist/registry.js +34 -2
  148. package/dist/registry.js.map +1 -1
  149. package/dist/remediate-pipeline.d.ts +3 -0
  150. package/dist/remediate-pipeline.d.ts.map +1 -1
  151. package/dist/remediate-pipeline.js +71 -0
  152. package/dist/remediate-pipeline.js.map +1 -1
  153. package/dist/remediate-request.js.map +1 -1
  154. package/dist/remediation.d.ts +9 -3
  155. package/dist/remediation.d.ts.map +1 -1
  156. package/dist/remediation.js +17 -5
  157. package/dist/remediation.js.map +1 -1
  158. package/dist/report.d.ts +9 -0
  159. package/dist/report.d.ts.map +1 -1
  160. package/dist/report.js +38 -0
  161. package/dist/report.js.map +1 -1
  162. package/dist/scan-worker.js.map +1 -1
  163. package/dist/scan.d.ts.map +1 -1
  164. package/dist/scan.js +8 -1
  165. package/dist/scan.js.map +1 -1
  166. package/dist/severity.js.map +1 -1
  167. package/dist/standards.d.ts +96 -0
  168. package/dist/standards.d.ts.map +1 -0
  169. package/dist/standards.js +107 -0
  170. package/dist/standards.js.map +1 -0
  171. package/dist/triage.js.map +1 -1
  172. package/dist/types.d.ts +2 -2
  173. package/dist/types.d.ts.map +1 -1
  174. package/dist/types.js.map +1 -1
  175. package/dist/verify.js.map +1 -1
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/version.js.map +1 -1
  179. package/dist/walk.js.map +1 -1
  180. package/dist/worktree.js.map +1 -1
  181. package/package.json +1 -1
  182. package/src/cbom-merge.ts +116 -0
  183. package/src/comments.ts +183 -18
  184. package/src/dependencies.ts +144 -4
  185. package/src/detect-utils.ts +47 -6
  186. package/src/detectors/c.ts +411 -5
  187. package/src/detectors/cicd.ts +158 -0
  188. package/src/detectors/cloud-kms.ts +87 -0
  189. package/src/detectors/cloudformation.ts +215 -0
  190. package/src/detectors/csharp.ts +154 -3
  191. package/src/detectors/database.ts +88 -0
  192. package/src/detectors/dnssec.ts +186 -0
  193. package/src/detectors/elixir.ts +237 -0
  194. package/src/detectors/go.ts +209 -6
  195. package/src/detectors/java.ts +191 -4
  196. package/src/detectors/jose.ts +94 -0
  197. package/src/detectors/jwk.ts +128 -0
  198. package/src/detectors/k8s.ts +112 -0
  199. package/src/detectors/mesh.ts +154 -0
  200. package/src/detectors/messaging.ts +98 -0
  201. package/src/detectors/pem.ts +54 -2
  202. package/src/detectors/php.ts +225 -0
  203. package/src/detectors/python.ts +153 -3
  204. package/src/detectors/ruby.ts +138 -7
  205. package/src/detectors/rust.ts +282 -7
  206. package/src/detectors/secrets.ts +114 -0
  207. package/src/detectors/source.ts +382 -72
  208. package/src/detectors/stateful-hbs.ts +180 -0
  209. package/src/detectors/terraform.ts +154 -0
  210. package/src/detectors/vpn.ts +131 -0
  211. package/src/evidence.ts +135 -0
  212. package/src/index.ts +15 -1
  213. package/src/inventory.ts +31 -1
  214. package/src/policy.ts +180 -0
  215. package/src/redact.ts +41 -0
  216. package/src/registry.ts +34 -2
  217. package/src/remediate-pipeline.ts +78 -0
  218. package/src/remediation.ts +23 -8
  219. package/src/report.ts +48 -2
  220. package/src/scan.ts +13 -1
  221. package/src/standards.ts +186 -0
  222. package/src/types.ts +3 -1
  223. package/src/version.ts +1 -1
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Config / source-agnostic detector: stateful hash-based signatures
3
+ * (NIST SP 800-208) — LMS, HSS, XMSS and XMSSMT.
4
+ *
5
+ * These schemes ARE quantum-safe and NIST-approved (SP 800-208, for firmware /
6
+ * secure-boot signing), so this is NOT a "broken crypto" finding. The hazard it
7
+ * flags is STATE MANAGEMENT: each private key holds a bounded set of one-time
8
+ * key indices, and reusing an index is catastrophic (it enables signature
9
+ * forgery). The detector therefore surfaces the usage so a reviewer can confirm
10
+ * rigorous state handling exists — or migrate to a stateless scheme.
11
+ *
12
+ * Like {@link pemDetector}, this is a `scope: "config"`, `language: "any"`
13
+ * detector that runs on every text file, because the distinctive LMS/XMSS
14
+ * parameter strings and library tokens (`LMS_SHA256_M32_H10`, `pyhsslms`,
15
+ * `XMSS-SHA2_10_256`, …) appear identically across languages, config, and docs.
16
+ * Every rule's metadata lives in the {@link RuleMeta} declaration below; the
17
+ * regexes are tightly bounded and `\b`-anchored to stay high-signal.
18
+ */
19
+ import type { Detector, Finding, RuleMeta } from "../types.js";
20
+ import { eachMatch, findingFromRule } from "../detect-utils.js";
21
+ import { CWE_RISKY_PRIMITIVE } from "../cwe.js";
22
+
23
+ /**
24
+ * Shared remediation for every stateful-HBS finding: these are approved but
25
+ * STATEFUL, so the reviewer must confirm state management before trusting them.
26
+ */
27
+ const STATEFUL_HBS_REMEDIATION =
28
+ "LMS/HSS/XMSS/XMSSMT are NIST-approved (SP 800-208) but STATEFUL: the signer " +
29
+ "must NEVER reuse a one-time key index (reuse enables signature forgery). Use " +
30
+ "only with rigorous, crash-safe state management; otherwise prefer the " +
31
+ "stateless ML-DSA (FIPS 204) or SLH-DSA (FIPS 205).";
32
+
33
+ /** A stateful-HBS rule: its catalog metadata plus the token regex that triggers it. */
34
+ interface HbsRule {
35
+ /** Global regex matching the distinctive token. */
36
+ re: RegExp;
37
+ meta: RuleMeta;
38
+ }
39
+
40
+ const HBS_RULES: HbsRule[] = [
41
+ {
42
+ // LMS parameter set, e.g. LMS_SHA256_M32_H10 / LMS_SHAKE_M24_H10 (SP 800-208
43
+ // adds SHAKE256 and the 192-bit M24/N24 sets to RFC 8554's SHA-256 sets).
44
+ re: /\bLMS_(?:SHA256|SHAKE(?:256)?)_[MN]\d+_[HW]\d+\b/g,
45
+ meta: {
46
+ id: "stateful-hbs-lms-param",
47
+ title: "LMS parameter set (stateful hash-based signature)",
48
+ description: "LMS/HSS one-time-signature parameter string (SP 800-208)",
49
+ category: "signature",
50
+ severity: "medium",
51
+ confidence: "high",
52
+ algorithm: "unknown",
53
+ hndl: false,
54
+ cwe: CWE_RISKY_PRIMITIVE,
55
+ message:
56
+ "LMS parameter set — NIST-approved (SP 800-208) but STATEFUL: reusing a one-time key index is catastrophic.",
57
+ remediation: STATEFUL_HBS_REMEDIATION,
58
+ },
59
+ },
60
+ {
61
+ // HSS keygen (hierarchical LMS), e.g. pyhsslms.hss_generate_private_key(...).
62
+ re: /\bhss_generate_private_key\b/g,
63
+ meta: {
64
+ id: "stateful-hbs-hss-keygen",
65
+ title: "HSS private-key generation (stateful hash-based signature)",
66
+ description: "HSS (hierarchical LMS) private-key generation call (SP 800-208)",
67
+ category: "signature",
68
+ severity: "medium",
69
+ confidence: "high",
70
+ algorithm: "unknown",
71
+ hndl: false,
72
+ cwe: CWE_RISKY_PRIMITIVE,
73
+ message:
74
+ "HSS private-key generation — NIST-approved (SP 800-208) but STATEFUL: never reuse a one-time key index.",
75
+ remediation: STATEFUL_HBS_REMEDIATION,
76
+ },
77
+ },
78
+ {
79
+ // pyhsslms — the Python LMS/HSS library import token.
80
+ re: /\bpyhsslms\b/g,
81
+ meta: {
82
+ id: "stateful-hbs-pyhsslms",
83
+ title: "pyhsslms library (stateful LMS/HSS signatures)",
84
+ description: "Reference to the pyhsslms LMS/HSS library (SP 800-208)",
85
+ category: "signature",
86
+ severity: "medium",
87
+ confidence: "high",
88
+ algorithm: "unknown",
89
+ hndl: false,
90
+ cwe: CWE_RISKY_PRIMITIVE,
91
+ message:
92
+ "pyhsslms (LMS/HSS) — NIST-approved (SP 800-208) but STATEFUL: the signer must never reuse a one-time key index.",
93
+ remediation: STATEFUL_HBS_REMEDIATION,
94
+ },
95
+ },
96
+ {
97
+ // XMSS parameter set, e.g. XMSS-SHA2_10_256 / XMSS-SHAKE256_10_192 (SP 800-208
98
+ // adds the SHAKE256 and 192-bit variants to RFC 8391's SHA-2/256 sets).
99
+ re: /\bXMSS-(?:SHA2|SHAKE(?:256)?)_\d+_(?:192|256)\b/g,
100
+ meta: {
101
+ id: "stateful-hbs-xmss-param",
102
+ title: "XMSS parameter set (stateful hash-based signature)",
103
+ description: "XMSS one-time-signature parameter string (SP 800-208)",
104
+ category: "signature",
105
+ severity: "medium",
106
+ confidence: "high",
107
+ algorithm: "unknown",
108
+ hndl: false,
109
+ cwe: CWE_RISKY_PRIMITIVE,
110
+ message:
111
+ "XMSS parameter set — NIST-approved (SP 800-208) but STATEFUL: reusing a one-time key index is catastrophic.",
112
+ remediation: STATEFUL_HBS_REMEDIATION,
113
+ },
114
+ },
115
+ {
116
+ // XMSSMT (multi-tree XMSS) parameter set, e.g. XMSSMT-SHA2_20/2_256 or the
117
+ // SP 800-208 SHAKE256 variant XMSSMT-SHAKE256_20/2_256.
118
+ re: /\bXMSSMT-(?:SHA2|SHAKE(?:256)?)_\d+\b/g,
119
+ meta: {
120
+ id: "stateful-hbs-xmssmt-param",
121
+ title: "XMSSMT parameter set (stateful hash-based signature)",
122
+ description: "XMSSMT (multi-tree XMSS) parameter string (SP 800-208)",
123
+ category: "signature",
124
+ severity: "medium",
125
+ confidence: "high",
126
+ algorithm: "unknown",
127
+ hndl: false,
128
+ cwe: CWE_RISKY_PRIMITIVE,
129
+ message:
130
+ "XMSSMT parameter set — NIST-approved (SP 800-208) but STATEFUL: never reuse a one-time key index.",
131
+ remediation: STATEFUL_HBS_REMEDIATION,
132
+ },
133
+ },
134
+ {
135
+ // XMSS keypair generation, e.g. xmss_keypair(...) (liboqs / xmss reference).
136
+ re: /\bxmss_keypair\b/g,
137
+ meta: {
138
+ id: "stateful-hbs-xmss-keypair",
139
+ title: "XMSS keypair generation (stateful hash-based signature)",
140
+ description: "XMSS keypair-generation call (SP 800-208)",
141
+ category: "signature",
142
+ severity: "medium",
143
+ confidence: "high",
144
+ algorithm: "unknown",
145
+ hndl: false,
146
+ cwe: CWE_RISKY_PRIMITIVE,
147
+ message:
148
+ "XMSS keypair generation — NIST-approved (SP 800-208) but STATEFUL: the signer must never reuse a one-time key index.",
149
+ remediation: STATEFUL_HBS_REMEDIATION,
150
+ },
151
+ },
152
+ ];
153
+
154
+ /** Detects stateful hash-based signature (SP 800-208) usage in arbitrary files. */
155
+ export const statefulHbsDetector: Detector = {
156
+ id: "stateful-hbs",
157
+ description:
158
+ "Stateful hash-based signatures (NIST SP 800-208: LMS / HSS / XMSS / XMSSMT) in any file",
159
+ scope: "config",
160
+ language: "any",
161
+ rules: HBS_RULES.map((r) => r.meta),
162
+ // Applies to every text file; the walker already filters out binaries.
163
+ appliesTo: () => true,
164
+ detect({ file, content }): Finding[] {
165
+ const findings: Finding[] = [];
166
+ for (const rule of HBS_RULES) {
167
+ eachMatch(rule.re, content, (m) => {
168
+ findings.push(
169
+ findingFromRule(rule.meta, {
170
+ file,
171
+ content,
172
+ index: m.index,
173
+ matchLength: m[0].length,
174
+ }),
175
+ );
176
+ });
177
+ }
178
+ return findings;
179
+ },
180
+ };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * IaC detector: classical asymmetric cryptography provisioned by Terraform /
3
+ * OpenTofu (`.tf`, `.tf.json`). Infrastructure-as-code mints real keys and CMKs
4
+ * that never appear in application source, so this is a distinct surface from the
5
+ * language packs.
6
+ *
7
+ * Covered:
8
+ * - hashicorp/tls `tls_private_key`: `algorithm = "RSA" | "ECDSA"`
9
+ * - Google Cloud KMS `version_template`: `algorithm = "RSA_SIGN_…" | "EC_SIGN_…"`
10
+ * - AWS KMS `aws_kms_key`: `customer_master_key_spec = "RSA_…" | "ECC_…"`
11
+ * - Azure `azurerm_key_vault_key`: `key_type = "RSA" | "EC"` (+ HSM variants)
12
+ *
13
+ * The HCL assignment forms (`algorithm = "…"`, `customer_master_key_spec = "…"`,
14
+ * `key_type = "…"`) are specific enough inside `.tf` files that the false-positive
15
+ * risk is low. EC keys are classified conservatively as key-agreement-capable
16
+ * (hndl:true) since a provisioned EC key can feed ECDH.
17
+ */
18
+ import type { Detector, Finding, RuleMeta } from "../types.js";
19
+ import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
20
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
21
+
22
+ const TF_EXTENSIONS: readonly string[] = [".tf", ".tf.json"];
23
+
24
+ // Each attribute is matched in BOTH HCL (`algorithm = "RSA"`) and the `.tf.json`
25
+ // variant (`"algorithm": "RSA"`): an optional quote around the key, and `:` or
26
+ // `=` as the separator. The `(?<![\w"-])` lookbehind stops a longer attribute
27
+ // name (`my_algorithm`) from matching on its suffix.
28
+ // RSA: tls_private_key `"RSA"` plus the Google KMS `RSA_SIGN_…` / `RSA_DECRYPT_…`.
29
+ const RE_TF_RSA = /(?<![\w"-])"?algorithm"?\s*[:=]\s*"RSA(?:_[A-Z0-9_]+)?"/g;
30
+ // ECDSA / EC signing: tls_private_key `"ECDSA"` and Google KMS `"EC_SIGN_…"`.
31
+ const RE_TF_ECDSA = /(?<![\w"-])"?algorithm"?\s*[:=]\s*"(?:ECDSA|EC_SIGN_[A-Z0-9_]+)"/g;
32
+ // AWS KMS customer master key specs.
33
+ const RE_TF_KMS_RSA = /(?<![\w"-])"?customer_master_key_spec"?\s*[:=]\s*"RSA_\d+"/g;
34
+ const RE_TF_KMS_EC = /(?<![\w"-])"?customer_master_key_spec"?\s*[:=]\s*"ECC_[A-Z0-9_]+"/g;
35
+ // Azure Key Vault key type (incl. the `-HSM` variants).
36
+ const RE_TF_AZ_RSA = /(?<![\w"-])"?key_type"?\s*[:=]\s*"RSA(?:-HSM)?"/g;
37
+ const RE_TF_AZ_EC = /(?<![\w"-])"?key_type"?\s*[:=]\s*"EC(?:-HSM)?"/g;
38
+
39
+ const RULE_TF_RSA: RuleMeta = {
40
+ id: "tf-rsa-key",
41
+ title: "Terraform RSA key",
42
+ description: "Terraform tls_private_key / KMS RSA key material",
43
+ category: "kem",
44
+ severity: "high",
45
+ confidence: "high",
46
+ algorithm: "RSA",
47
+ hndl: true,
48
+ cwe: CWE_BROKEN_CRYPTO,
49
+ message: "Terraform provisions a classical RSA key, which is not quantum-safe.",
50
+ remediation: "Plan migration to PQC (ML-KEM-768 for encryption, ML-DSA-65 for signatures).",
51
+ };
52
+ const RULE_TF_ECDSA: RuleMeta = {
53
+ id: "tf-ecdsa-key",
54
+ title: "Terraform ECDSA key",
55
+ description: "Terraform tls_private_key / KMS EC signing key",
56
+ category: "signature",
57
+ severity: "high",
58
+ confidence: "high",
59
+ algorithm: "ECDSA",
60
+ hndl: false,
61
+ cwe: CWE_BROKEN_CRYPTO,
62
+ message: "Terraform provisions a classical ECDSA key, forgeable by a quantum attacker.",
63
+ remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
64
+ };
65
+ const RULE_TF_KMS_RSA: RuleMeta = {
66
+ id: "tf-kms-rsa",
67
+ title: "Terraform AWS KMS RSA CMK",
68
+ description: 'Terraform aws_kms_key customer_master_key_spec = "RSA_*"',
69
+ category: "kem",
70
+ severity: "high",
71
+ confidence: "high",
72
+ algorithm: "RSA",
73
+ hndl: true,
74
+ cwe: CWE_BROKEN_CRYPTO,
75
+ message:
76
+ "Terraform provisions a classical RSA KMS customer master key (harvest-now-decrypt-later exposed for encryption CMKs).",
77
+ remediation: "Plan migration to PQC as cloud KMS adds ML-KEM / ML-DSA key specs.",
78
+ };
79
+ const RULE_TF_KMS_EC: RuleMeta = {
80
+ id: "tf-kms-ec",
81
+ title: "Terraform AWS KMS EC CMK",
82
+ description: 'Terraform aws_kms_key customer_master_key_spec = "ECC_*"',
83
+ category: "key-exchange",
84
+ severity: "high",
85
+ confidence: "high",
86
+ algorithm: "ECDH",
87
+ hndl: true,
88
+ cwe: CWE_BROKEN_CRYPTO,
89
+ message:
90
+ "Terraform provisions a classical EC KMS customer master key; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
91
+ remediation:
92
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
93
+ };
94
+ const RULE_TF_AZ_RSA: RuleMeta = {
95
+ id: "tf-keyvault-rsa",
96
+ title: "Terraform Azure Key Vault RSA key",
97
+ description: 'Terraform azurerm_key_vault_key key_type = "RSA"',
98
+ category: "kem",
99
+ severity: "high",
100
+ confidence: "high",
101
+ algorithm: "RSA",
102
+ hndl: true,
103
+ cwe: CWE_BROKEN_CRYPTO,
104
+ message: "Terraform provisions a classical RSA Azure Key Vault key, which is not quantum-safe.",
105
+ remediation: "Plan migration to PQC (ML-KEM-768 / ML-DSA-65).",
106
+ };
107
+ const RULE_TF_AZ_EC: RuleMeta = {
108
+ id: "tf-keyvault-ec",
109
+ title: "Terraform Azure Key Vault EC key",
110
+ description: 'Terraform azurerm_key_vault_key key_type = "EC"',
111
+ category: "key-exchange",
112
+ severity: "high",
113
+ confidence: "high",
114
+ algorithm: "ECDH",
115
+ hndl: true,
116
+ cwe: CWE_BROKEN_CRYPTO,
117
+ message:
118
+ "Terraform provisions a classical EC Azure Key Vault key; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
119
+ remediation:
120
+ "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
121
+ };
122
+
123
+ /** Detects classical asymmetric crypto provisioned in Terraform / OpenTofu. */
124
+ export const terraformDetector: Detector = {
125
+ id: "terraform-crypto",
126
+ description: "Classical asymmetric crypto provisioned by Terraform / OpenTofu (IaC)",
127
+ scope: "config",
128
+ language: "any",
129
+ rules: [
130
+ RULE_TF_RSA,
131
+ RULE_TF_ECDSA,
132
+ RULE_TF_KMS_RSA,
133
+ RULE_TF_KMS_EC,
134
+ RULE_TF_AZ_RSA,
135
+ RULE_TF_AZ_EC,
136
+ ],
137
+ appliesTo: (f) => hasExtension(f, TF_EXTENSIONS),
138
+ detect({ file, content }): Finding[] {
139
+ const findings: Finding[] = [];
140
+ const add = (re: RegExp, rule: RuleMeta) =>
141
+ eachMatch(re, content, (m) =>
142
+ findings.push(
143
+ findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
144
+ ),
145
+ );
146
+ add(RE_TF_RSA, RULE_TF_RSA);
147
+ add(RE_TF_ECDSA, RULE_TF_ECDSA);
148
+ add(RE_TF_KMS_RSA, RULE_TF_KMS_RSA);
149
+ add(RE_TF_KMS_EC, RULE_TF_KMS_EC);
150
+ add(RE_TF_AZ_RSA, RULE_TF_AZ_RSA);
151
+ add(RE_TF_AZ_EC, RULE_TF_AZ_EC);
152
+ return findings;
153
+ },
154
+ };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Config detector: classical key exchange in network transport / VPN config —
3
+ * WireGuard, IPsec (strongSwan), and OpenSSH server config. These are the tunnels
4
+ * carrying "communication between things"; their key exchange is classical and so
5
+ * harvest-now-decrypt-later exposed (a recorded tunnel is decryptable once a CRQC
6
+ * exists).
7
+ *
8
+ * Covered (each gated so it only fires inside the relevant config):
9
+ * - **WireGuard** — `[Interface]`/`[Peer]` sections with `PrivateKey`/`PublicKey`.
10
+ * WireGuard's Noise handshake is Curve25519 with NO standard PQC option, so this
11
+ * is an especially sharp finding: it cannot be made quantum-safe without a
12
+ * tunnel-wrapping layer. The private key is treated as sensitive key material.
13
+ * - **IPsec / strongSwan** — IKE/ESP proposals naming classical DH groups
14
+ * (`modp*` = finite-field DH, `ecp*` = ECDH).
15
+ * - **sshd_config / ssh_config** — a `KexAlgorithms` line that offers no PQC hybrid
16
+ * KEX (`sntrup761x25519-*`, `mlkem768x25519-*`). If a PQC KEX IS listed the line
17
+ * is left alone, so a hardened server stays silent.
18
+ */
19
+ import type { Detector, Finding, RuleMeta } from "../types.js";
20
+ import { eachMatch, findingFromRule } from "../detect-utils.js";
21
+ import { CWE_BROKEN_CRYPTO } from "../cwe.js";
22
+
23
+ const RE_WG_KEY = /\b(?:PrivateKey|PublicKey)\s*=\s*[A-Za-z0-9+/]{42,}=/g;
24
+ const RE_IPSEC_MODP = /\bmodp\d+\b/gi;
25
+ const RE_IPSEC_ECP = /\becp\d+(?:bp)?\b/gi;
26
+ const RE_SSHD_KEX = /^[ \t]*KexAlgorithms[ \t]+([^\n]*)/gm;
27
+
28
+ const RULE_WG: RuleMeta = {
29
+ id: "net-wireguard-x25519",
30
+ title: "WireGuard Curve25519 key",
31
+ description: "WireGuard [Interface]/[Peer] key — Curve25519 Noise handshake (no PQC option)",
32
+ category: "key-exchange",
33
+ severity: "high",
34
+ confidence: "high",
35
+ algorithm: "X25519",
36
+ hndl: true,
37
+ cwe: CWE_BROKEN_CRYPTO,
38
+ sensitive: true,
39
+ message:
40
+ "WireGuard tunnel keyed by classical Curve25519 (Noise); WireGuard has no standard post-quantum KEM, so the tunnel is harvest-now-decrypt-later exposed until wrapped by a PQC layer.",
41
+ remediation:
42
+ "Wrap the tunnel in a PQC-hybrid transport (e.g. a TLS 1.3 X25519MLKEM768 layer) or track WireGuard PQC proposals; rotate keys when available.",
43
+ };
44
+ const RULE_IPSEC_DH: RuleMeta = {
45
+ id: "net-ipsec-modp-dh",
46
+ title: "IPsec classical DH group (modp)",
47
+ description: "IPsec/strongSwan IKE/ESP proposal names a finite-field DH group (modp*)",
48
+ category: "key-exchange",
49
+ severity: "medium",
50
+ confidence: "high",
51
+ algorithm: "DH",
52
+ hndl: true,
53
+ cwe: CWE_BROKEN_CRYPTO,
54
+ message:
55
+ "IPsec proposal uses a classical finite-field Diffie-Hellman group (modp*); the tunnel key exchange is harvest-now-decrypt-later exposed.",
56
+ remediation: "Add a PQC/hybrid IKE proposal (ML-KEM) as your IPsec stack supports it.",
57
+ };
58
+ const RULE_IPSEC_EC: RuleMeta = {
59
+ id: "net-ipsec-ecp-ecdh",
60
+ title: "IPsec classical ECDH group (ecp)",
61
+ description: "IPsec/strongSwan IKE/ESP proposal names an elliptic-curve DH group (ecp*)",
62
+ category: "key-exchange",
63
+ severity: "medium",
64
+ confidence: "high",
65
+ algorithm: "ECDH",
66
+ hndl: true,
67
+ cwe: CWE_BROKEN_CRYPTO,
68
+ message:
69
+ "IPsec proposal uses a classical elliptic-curve Diffie-Hellman group (ecp*); the tunnel key exchange is harvest-now-decrypt-later exposed.",
70
+ remediation:
71
+ "Add a PQC/hybrid IKE proposal (X25519MLKEM768 / ML-KEM) as your IPsec stack supports it.",
72
+ };
73
+ const RULE_SSHD_KEX: RuleMeta = {
74
+ id: "net-sshd-classical-kex",
75
+ title: "SSH server offers no PQC key exchange",
76
+ description:
77
+ "sshd_config KexAlgorithms lists only classical key exchange (no sntrup761/mlkem768)",
78
+ category: "key-exchange",
79
+ severity: "medium",
80
+ confidence: "high",
81
+ algorithm: "ECDH",
82
+ hndl: true,
83
+ cwe: CWE_BROKEN_CRYPTO,
84
+ message:
85
+ "SSH server's KexAlgorithms offers only classical key exchange (no sntrup761x25519 / mlkem768x25519); sessions are harvest-now-decrypt-later exposed.",
86
+ remediation: "Add a PQC hybrid KEX: sntrup761x25519-sha512@openssh.com or mlkem768x25519-sha256.",
87
+ };
88
+
89
+ /** True when a KexAlgorithms value already offers a post-quantum hybrid KEX. */
90
+ function offersPqKex(kexLine: string): boolean {
91
+ return /sntrup761x25519|mlkem768/.test(kexLine);
92
+ }
93
+
94
+ /** Detects classical key exchange in WireGuard / IPsec / sshd network config. */
95
+ export const vpnDetector: Detector = {
96
+ id: "network-transport-crypto",
97
+ description: "Classical key exchange in network transport / VPN config (WireGuard, IPsec, sshd)",
98
+ scope: "config",
99
+ language: "any",
100
+ rules: [RULE_WG, RULE_IPSEC_DH, RULE_IPSEC_EC, RULE_SSHD_KEX],
101
+ appliesTo: (f) => {
102
+ const lower = f.toLowerCase();
103
+ const base = lower.split("/").pop() ?? lower;
104
+ return lower.endsWith(".conf") || base === "sshd_config" || base === "ssh_config";
105
+ },
106
+ detect({ file, content }): Finding[] {
107
+ const findings: Finding[] = [];
108
+ const push = (rule: RuleMeta, index: number, length: number) =>
109
+ findings.push(findingFromRule(rule, { file, content, index, matchLength: length }));
110
+
111
+ // WireGuard: gated to a WireGuard config section.
112
+ if (content.includes("[Interface]") || content.includes("[Peer]")) {
113
+ eachMatch(RE_WG_KEY, content, (m) => push(RULE_WG, m.index, m[0].length));
114
+ }
115
+
116
+ // IPsec / strongSwan: gated to a proposal assignment.
117
+ if (/\b(?:ike|esp|proposals?|keyexchange)\s*=/i.test(content)) {
118
+ eachMatch(RE_IPSEC_MODP, content, (m) => push(RULE_IPSEC_DH, m.index, m[0].length));
119
+ eachMatch(RE_IPSEC_ECP, content, (m) => push(RULE_IPSEC_EC, m.index, m[0].length));
120
+ }
121
+
122
+ // sshd_config / ssh_config: KexAlgorithms line with no PQC hybrid offered.
123
+ const base = file.toLowerCase().split("/").pop() ?? "";
124
+ if (base === "sshd_config" || base === "ssh_config") {
125
+ eachMatch(RE_SSHD_KEX, content, (m) => {
126
+ if (!offersPqKex(m[1])) push(RULE_SSHD_KEX, m.index, m[0].length);
127
+ });
128
+ }
129
+ return findings;
130
+ },
131
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * ISO/IEC 27001:2022 Annex A 8.24 evidence-chain export
3
+ * (docs/compliance/iso27001-a8.24-evidence.md).
4
+ *
5
+ * Emits a self-describing readiness report bundling the scan result, crypto
6
+ * inventory, and CycloneDX CBOM, plus an attestation carrying a DETERMINISTIC
7
+ * content hash — the same scan over the same commit + config yields the same
8
+ * hash (the volatile scan timestamp is deliberately excluded from the hashed
9
+ * body). Signing + RFC-3161 timestamping are left to an EXTERNAL, vetted signer
10
+ * (ADR-0004: this project performs no cryptography itself — it orchestrates a
11
+ * signer, it does not implement one). SHA-256 here is an integrity hash (a Node
12
+ * built-in), not an asymmetric primitive.
13
+ *
14
+ * Honesty boundary: this artifact is EVIDENCE for A.8.24, not the control. The
15
+ * organization still owns the cryptography policy, key management, and the
16
+ * conformance judgment. A clean scan is the absence of detected candidates, not
17
+ * proof of quantum-safety (qScan is lexical). See docs/COMPLIANCE.md §3.
18
+ */
19
+ import { createHash } from "node:crypto";
20
+
21
+ import type { ScanResult } from "./types.js";
22
+ import { toCbom } from "./cbom.js";
23
+ import { VERSION } from "./version.js";
24
+ import { buildPolicyMapping } from "./policy.js";
25
+ import type { CryptoPolicy, PolicyMapping } from "./policy.js";
26
+
27
+ /** Stable per-finding record for the evidence body (deterministic per commit). */
28
+ export interface EvidenceFinding {
29
+ ruleId: string;
30
+ algorithm?: string;
31
+ severity: string;
32
+ hndl: boolean;
33
+ file: string;
34
+ line: number;
35
+ }
36
+
37
+ export interface ReadinessReport {
38
+ reportType: "quantakrypto-readiness";
39
+ specVersion: 1;
40
+ subject: {
41
+ repository: string | null;
42
+ commit: string | null;
43
+ scannedRoot: string;
44
+ scanTimeUtc: string;
45
+ };
46
+ tool: { name: "qScan"; version: string };
47
+ inventory: ScanResult["inventory"];
48
+ findings: EvidenceFinding[];
49
+ /** §4 policy verdicts, present only when a crypto policy was supplied. */
50
+ policyMapping?: PolicyMapping;
51
+ cbom: unknown;
52
+ attestation: {
53
+ /** sha256 over the canonicalized deterministic body (excludes scanTimeUtc). */
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;
59
+ };
60
+ }
61
+
62
+ /** Canonical JSON: object keys sorted recursively, so the hash is reproducible. */
63
+ function canonicalize(value: unknown): unknown {
64
+ if (Array.isArray(value)) return value.map(canonicalize);
65
+ if (value && typeof value === "object") {
66
+ const out: Record<string, unknown> = {};
67
+ for (const k of Object.keys(value as Record<string, unknown>).sort()) {
68
+ out[k] = canonicalize((value as Record<string, unknown>)[k]);
69
+ }
70
+ return out;
71
+ }
72
+ return value;
73
+ }
74
+
75
+ export interface ReadinessReportOptions {
76
+ /** Repository URL (e.g. from `GITHUB_REPOSITORY`); omitted → null. */
77
+ repository?: string;
78
+ /** Full commit SHA (e.g. from `GITHUB_SHA`); omitted → null. */
79
+ commit?: string;
80
+ /** Optional org cryptography policy — adds the §4 `policyMapping` verdicts. */
81
+ policy?: CryptoPolicy;
82
+ }
83
+
84
+ /**
85
+ * Build the A.8.24 readiness report for a scan result. The attestation's
86
+ * `contentHash` covers everything EXCEPT the scan timestamp and the attestation
87
+ * block itself, so re-running the same scan on the same commit is verifiable.
88
+ */
89
+ export function buildReadinessReport(
90
+ result: ScanResult,
91
+ opts: ReadinessReportOptions = {},
92
+ ): ReadinessReport {
93
+ const findings: EvidenceFinding[] = result.findings.map((f) => ({
94
+ ruleId: f.ruleId,
95
+ ...(f.algorithm ? { algorithm: f.algorithm } : {}),
96
+ severity: f.severity,
97
+ hndl: f.hndl,
98
+ file: f.location.file,
99
+ line: f.location.line,
100
+ }));
101
+
102
+ // The CBOM is a deterministic *view* of the (hashed) findings + inventory, but
103
+ // its CycloneDX envelope carries a volatile timestamp/serial — so it is
104
+ // EXCLUDED from the hashed body (its integrity follows from its hashed inputs)
105
+ // to keep the content hash reproducible across scan runs on the same commit.
106
+ // §4: if the org supplied a crypto policy, attest the per-finding verdicts too.
107
+ // Deterministic (same findings + policy → same mapping), so it is hashed.
108
+ const policyMapping = opts.policy ? buildPolicyMapping(result.findings, opts.policy) : undefined;
109
+
110
+ const hashableBody = {
111
+ reportType: "quantakrypto-readiness",
112
+ specVersion: 1,
113
+ subject: {
114
+ repository: opts.repository ?? null,
115
+ commit: opts.commit ?? null,
116
+ scannedRoot: result.root,
117
+ },
118
+ tool: { name: "qScan", version: VERSION },
119
+ inventory: result.inventory,
120
+ findings,
121
+ ...(policyMapping ? { policyMapping } : {}),
122
+ };
123
+ const contentHash =
124
+ "sha256:" +
125
+ createHash("sha256")
126
+ .update(JSON.stringify(canonicalize(hashableBody)))
127
+ .digest("hex");
128
+
129
+ return {
130
+ ...hashableBody,
131
+ subject: { ...hashableBody.subject, scanTimeUtc: result.finishedAt },
132
+ cbom: toCbom(result),
133
+ attestation: { contentHash, timestamp: null, signature: null },
134
+ } as ReadinessReport;
135
+ }
package/src/index.ts CHANGED
@@ -104,12 +104,21 @@ export { vulnerableDependencies, DEP_VULNERABLE_RULE, isManifestFile } from "./d
104
104
  export { SEVERITY_ORDER, severityRank, meetsThreshold, sarifLevel } from "./severity.js";
105
105
 
106
106
  // Reporters.
107
- export { toSarif, toJson, formatSummary } from "./report.js";
107
+ export { toSarif, toJson, formatSummary, formatTierGuidance } from "./report.js";
108
108
  export type { ReportOptions } from "./report.js";
109
109
 
110
110
  // CycloneDX 1.6 cryptographic bill of materials (CBOM) export.
111
111
  export { toCbom } from "./cbom.js";
112
112
  export type { CycloneDxBom, CbomComponent } from "./cbom.js";
113
+ // Merge multiple CBOMs (code + infra + live endpoints) into one combined BOM.
114
+ export { mergeCboms } from "./cbom-merge.js";
115
+ // ISO/IEC 27001 A.8.24 evidence-chain readiness report.
116
+ export { buildReadinessReport } from "./evidence.js";
117
+ export type { ReadinessReport, EvidenceFinding, ReadinessReportOptions } from "./evidence.js";
118
+
119
+ // Cryptography policy → per-finding verdicts (A.8.24 evidence §4).
120
+ export { buildPolicyMapping, parseCryptoPolicy, verdictForAlgorithm } from "./policy.js";
121
+ export type { CryptoPolicy, PolicyVerdict, PolicyMapping, PolicyFindingVerdict } from "./policy.js";
113
122
 
114
123
  // Remediation lookup (family + tier-aware) and stateful-HBS guidance.
115
124
  export {
@@ -117,10 +126,15 @@ export {
117
126
  remediationForTier,
118
127
  TIER_PARAMS,
119
128
  STATEFUL_HBS_NOTE,
129
+ PQC_TRANSITION_NOTE,
120
130
  statefulHbsApplies,
121
131
  } from "./remediation.js";
122
132
  export type { SecurityTier } from "./remediation.js";
123
133
 
134
+ // Post-quantum standards source of truth + review cadence.
135
+ export { PQC_STANDARDS, standardsReviewStatus } from "./standards.js";
136
+ export type { PqcStandards, StandardsCitation, StandardsReviewStatus } from "./standards.js";
137
+
124
138
  // CWE identifier constants.
125
139
  export {
126
140
  CWE_BROKEN_CRYPTO,