@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
package/README.md CHANGED
@@ -58,7 +58,7 @@ interface ScanOptions {
58
58
  exclude?: string[]; // extra exclude patterns (substring/prefix)
59
59
  noDefaultIgnores?: boolean; // disable node_modules/.git/dist/… ignores
60
60
  source?: boolean; // scan source files (default true)
61
- dependencies?: boolean; // scan manifests/lockfiles across 6 ecosystems (default true)
61
+ dependencies?: boolean; // scan manifests/lockfiles across 7 ecosystems (default true)
62
62
  config?: boolean; // scan PEM/TLS/cert config (default true)
63
63
  maxFileSize?: number; // bytes; default 2 MiB (manifests are exempt)
64
64
  scanMinified?: boolean; // scan minified/generated files (default false: skip them)
@@ -139,8 +139,9 @@ drives the source/config scope toggles from the detector's **declared `scope`**
139
139
  | `ssh-cert` | config | SSH public keys (`ssh-rsa`, `ssh-ed25519`, `ecdsa-sha2-*`) and X.509 certificate signature algorithms (`sha256WithRSAEncryption`, `ecdsa-with-SHA256`, …) |
140
140
 
141
141
  The rows above cover JavaScript/TypeScript plus the language-agnostic PEM/SSH/TLS
142
- surfaces. Seven further **language packs** apply the same RSA/EC/DSA/DH/Ed25519
143
- detection to other ecosystems (each is a single umbrella detector):
142
+ surfaces. Thirteen further **language packs** apply the same RSA/EC/DSA/DH/Ed25519
143
+ detection to other ecosystems including a smart-contract pack (Solidity/Move/
144
+ Cairo on-chain signature verification) — each a single umbrella detector:
144
145
 
145
146
  | Detector | Scope | Language |
146
147
  | --- | --- | --- |
@@ -150,7 +151,12 @@ detection to other ecosystems (each is a single umbrella detector):
150
151
  | `csharp-crypto` | source | C#/.NET (`RSA`, `ECDsa`, `ECDiffieHellman`, `DSA`) |
151
152
  | `rust-crypto` | source | Rust (`rsa`, `p256`/`k256` ECDSA, `x25519-dalek`, `ed25519-dalek`) |
152
153
  | `ruby-crypto` | source | Ruby (`OpenSSL::PKey::{RSA,EC,DSA,DH}`) |
154
+ | `php-crypto` | source | PHP (`openssl_pkey_new`, phpseclib `RSA`/`EC`/`DSA`) |
155
+ | `elixir-crypto` | source | Elixir/Erlang (`:crypto`, `:public_key` RSA/ECDSA/EdDSA) |
153
156
  | `c-crypto` | source | C/C++ (OpenSSL `EVP_PKEY_*` / `RSA_*` / `EC_*` / `DSA_*` / `DH_*`) |
157
+ | `swift-crypto` | source | Swift (CryptoKit + Apple Security `SecKey*`) |
158
+ | `objc-crypto` | source | Objective-C (`.m`/`.mm`, Apple Security `SecKey*`) |
159
+ | `dart-crypto` | source | Dart/Flutter (pointycastle, `cryptography` — RSA/EC/Ed25519/X25519) |
154
160
 
155
161
  `defaultRegistry` is a `DetectorRegistry` preloaded with these built-ins. The
156
162
  registry is the plugin point:
@@ -165,33 +171,54 @@ const result = await scan({ root: ".", detectors: registry.all() });
165
171
  `DetectorRegistry` exposes `register(d)`, `get(id)`, `has(id)`, `all()` and
166
172
  `clone()`. Ids must be unique (duplicate registration throws).
167
173
 
168
- #### Adding a detector / language
174
+ > **Public plugin surface vs. internal helpers.** The stable, frozen way to add a
175
+ > detector from *outside* this package is the `Detector` interface plus the
176
+ > `scan({ detectors })` / `DetectorRegistry` hook shown above — build your rule
177
+ > metadata with the public `RuleMeta` type and return `Finding[]`. The lexical
178
+ > conveniences the in-repo detectors use (`findingFromRule`, `eachMatch`, the
179
+ > comment maskers, `hasExtension`, …) are **internal** and intentionally *not*
180
+ > part of the frozen API: they churn with the scanner internals. The step-by-step
181
+ > guide below is for **in-repo contributors** (it edits `registry.ts`,
182
+ > `detect-utils.ts`, `comments.ts`), who can import those helpers directly.
183
+
184
+ #### Adding a detector / language (in-repo contributors)
169
185
 
170
186
  1. Create `src/detectors/<lang>.ts` exporting one or more `Detector`s. Set
171
- `language` (`"js" | "python" | "go" | "java" | "csharp" | "rust" | "ruby" | "c" | "any"`),
172
- `scope` (`"source" | "config"`), an `appliesTo(path)` extension check, and a pure
173
- `detect({ file, content })` returning `Finding[]` (use `makeFinding` from
174
- `detect-utils` for consistent location/remediation/CWE handling).
175
- 2. If the language uses new file extensions, ensure the walker treats them as
176
- text (they are scanned unless listed as binary in `walk.ts`).
177
- 3. For a new dependency ecosystem, extend `VulnerableDependency.ecosystem` and
178
- add a manifest matcher in `dependencies.ts`.
179
- 4. Register it: `defaultRegistry.register(myDetector)`, or pass
180
- `{ detectors: [...] }` to `scan`. No edit to `scan()` is required — scope is
181
- honoured from the detector's declared `scope`.
187
+ `language` (a member of `DetectorLanguage` in `types.ts` add a new one for a new
188
+ language pack), `scope` (`"source" | "config"`), an `appliesTo(path)` extension
189
+ check, and a pure `detect({ file, content })` returning `Finding[]`. Build findings
190
+ with `findingFromRule(ruleMeta, at, overrides?)` (not `makeFinding`) so every rule's
191
+ metadata lives once in its `RuleMeta` catalog entry.
192
+ 2. Define `<LANG>_EXTENSIONS` in `detect-utils.ts` next to the other packs (do **not**
193
+ inline the list in the detector) and gate `appliesTo` on it.
194
+ 3. **Wire the new SOURCE language into coverage + comment handling** (skipping this is
195
+ how a pack half-lands): add the extensions to `ANALYZABLE_SOURCE_EXTENSIONS` and the
196
+ name to `ANALYZABLE_LANGUAGES_LABEL` (`detect-utils.ts`) so `analyzedFiles`/coverage
197
+ count it; and add them to the comment table in `comments.ts` (`C_LIKE` for
198
+ `//`+`/* */`, `HASH_LIKE` for `#`) so commented-out code is suppressed. A `config`
199
+ detector instead masks its own comment lines with `maskCommentLines` /
200
+ `maskBlockComments`.
201
+ 4. Register it in `builtinDetectors` (`registry.ts`) — the single source of truth the
202
+ default registry and public `detectors` export are built from. (Ad-hoc: pass
203
+ `{ detectors: [...] }` to `scan`.) No edit to `scan()` is needed; scope is honoured
204
+ from the detector's declared `scope`.
205
+ 5. Add tests: at least one positive per rule, one negative/FP-bait, and a gating test;
206
+ consider a labelled benchmark corpus fixture (`test/benchmark/`). For a new dependency
207
+ ecosystem, also extend `VulnerableDependency.ecosystem` + a matcher in `dependencies.ts`.
182
208
 
183
209
  ### `vulnerableDependencies: VulnerableDependency[]`
184
210
 
185
- Curated database (**61 entries**) of packages whose purpose is classical
186
- asymmetric crypto, spanning **six ecosystems** — npm, PyPI, Cargo, Go modules,
187
- Maven, and RubyGems (`VulnerableDependency.ecosystem`). The npm subset includes
211
+ Curated database (**77 entries**) of packages whose purpose is classical
212
+ asymmetric crypto, spanning **seven ecosystems** — npm, PyPI, Cargo, Go modules,
213
+ Maven, RubyGems, and NuGet (`VulnerableDependency.ecosystem`). The npm subset includes
188
214
  `node-forge`, `elliptic`, `jsrsasign`, `node-rsa`, `ursa`, `sshpk`,
189
215
  `jsonwebtoken`, `jose`, `jws`, `eccrypto`, `secp256k1`, `tweetnacl`, `ed25519`,
190
216
  `@noble/curves`, `@noble/secp256k1`, `@noble/ed25519`, `paseto`, `bcrypto`,
191
217
  `ecpair`, `keypair`. `scan()` matches these against each ecosystem's manifests
192
218
  and lockfiles — `package.json` / `package-lock.json` / `yarn.lock` /
193
219
  `pnpm-lock.yaml`, `requirements.txt` / `pyproject.toml` / `Pipfile`, `Cargo.toml`,
194
- `go.mod`, `pom.xml` / `build.gradle`, `Gemfile` / `*.gemspec` — and emits
220
+ `go.mod`, `pom.xml` / `build.gradle`, `Gemfile` / `*.gemspec`, `*.csproj` /
221
+ `packages.config` / `directory.packages.props` — and emits
195
222
  `category: "dependency"` findings located at the manifest.
196
223
 
197
224
  ### `buildInventory(findings: Finding[]): CryptoInventory`
@@ -250,9 +277,30 @@ const { newFindings, suppressed } = applyBaseline(result.findings, baseline);
250
277
  ### `toCbom(result): CycloneDxBom`
251
278
 
252
279
  A CycloneDX 1.6 **cryptographic bill of materials** (CBOM): one
253
- `cryptographic-asset` component per distinct (algorithm, primitive) pair, with
254
- occurrence evidence and `quantumVulnerable` / `harvestNowDecryptLater` flags.
255
- Deterministic output.
280
+ `cryptographic-asset` component per distinct (assetType, algorithm, discriminator),
281
+ with occurrence evidence and `quantumVulnerable` / `harvestNowDecryptLater` flags.
282
+ Each finding is classified into its proper CycloneDX `assetType` — `algorithm`
283
+ (crypto usage), `certificate` (X.509), `related-crypto-material` (private/public
284
+ key material), or `protocol` (TLS). Deterministic output.
285
+
286
+ ### `toOpenVex(result, opts?): OpenVexDocument`
287
+
288
+ An **OpenVEX 0.2.0** document: one statement per rule (a synthetic `QK-<ruleId>`
289
+ vulnerability, since PQC findings have no CVE), every affected `file:line` product,
290
+ `status: "affected"`, the rule's remediation as `action_statement`, and any
291
+ `--triage` verdict in `status_notes`. Deterministic output; feeds VEX pipelines.
292
+
293
+ ### Evidence chain — `buildReadinessReport` / `signReadinessReport` / `verifyReadinessReport`
294
+
295
+ The ISO/IEC 27001 A.8.24 readiness report (see
296
+ [docs/compliance/iso27001-a8.24-evidence.md](../../docs/compliance/iso27001-a8.24-evidence.md)):
297
+
298
+ - `buildReadinessReport(result, opts?)` — bundles the scan result, inventory, CBOM,
299
+ and a **deterministic** `attestation.contentHash` (excludes the volatile scan time).
300
+ - `signReadinessReport(report, { signer?, timestamper? })` — orchestrates an EXTERNAL
301
+ signer/timestamper over the `contentHash` (ADR-0004; the tool implements no crypto).
302
+ - `verifyReadinessReport(report)` — recomputes the content hash to detect tampering
303
+ with any attested field, returning `{ valid, computedHash, claimedHash, reason? }`.
256
304
 
257
305
  ### CWE tagging
258
306
 
@@ -274,14 +322,15 @@ Tool version surfaced in reports (kept in sync with `package.json`).
274
322
  | `changedFiles` | fn | Git-aware changed-files list for incremental scans |
275
323
  | `detectFile` | fn | Pure per-file detect (used by workers / tests) |
276
324
  | `compareFindings` | fn | Stable finding comparator (file → line → ruleId) |
277
- | `mergeChunkResults`, `chunkByBytes` | fn | Pure parallel merge / byte-chunking helpers |
278
- | `walkFiles`, `isBinaryPath`, `isGeneratedPath`, `looksMinified` | fn | Walker + file-classification helpers |
325
+ | `walkFiles`, `isBinaryPath`, `looksMinified` | fn | Walker + file-classification helpers |
279
326
  | `detectors` | const | Built-in detector array (mirrors `defaultRegistry.all()`) |
280
- | `DetectorRegistry`, `defaultRegistry`, `detectorScope` | class/const/fn | Detector plugin point |
327
+ | `DetectorRegistry`, `defaultRegistry` | class/const | Detector plugin point |
281
328
  | `buildInventory` | fn | Aggregate findings → `CryptoInventory` |
282
- | `vulnerableDependencies` | const | Curated quantum-vulnerable npm DB |
329
+ | `vulnerableDependencies` | const | Curated quantum-vulnerable dependency DB (7 ecosystems) |
283
330
  | `toSarif`, `toJson`, `formatSummary` | fn | Reporters (SARIF 2.1.0 / JSON / human) |
284
- | `toCbom` | fn | CycloneDX 1.6 CBOM export |
331
+ | `toCbom` | fn | CycloneDX 1.6 CBOM export (assetType-classified) |
332
+ | `toOpenVex` | fn | OpenVEX 0.2.0 export |
333
+ | `buildReadinessReport`, `signReadinessReport`, `verifyReadinessReport` | fn | ISO A.8.24 evidence chain (build / sign / verify) |
285
334
  | `remediationFor`, `remediationForTier`, `TIER_PARAMS` | fn/const | PQC remediation (family + CNSA tier) |
286
335
  | `STATEFUL_HBS_NOTE`, `statefulHbsApplies` | const/fn | SP 800-208 LMS/XMSS guidance |
287
336
  | `fingerprintFinding`, `baselineFromFindings`, `applyBaseline`, `loadBaseline`, `saveBaseline`, `BASELINE_VERSION` | fn/const | Canonical baseline |
@@ -1 +1 @@
1
- {"version":3,"file":"cbom-merge.d.ts","sourceRoot":"","sources":["../src/cbom-merge.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,WAAW,CAAC;AA6B7D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,CAkEtE"}
1
+ {"version":3,"file":"cbom-merge.d.ts","sourceRoot":"","sources":["../src/cbom-merge.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,WAAW,CAAC;AA8C7D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,CA+EtE"}
@@ -17,8 +17,21 @@ function occurrencesOf(c) {
17
17
  const occ = ev?.occurrences;
18
18
  return Array.isArray(occ) ? occ : [];
19
19
  }
20
+ const HNDL_PROP = "quantakrypto:harvestNowDecryptLater";
20
21
  function hndlOf(c) {
21
- return (c.cryptoProperties.harvestNowDecryptLater === true);
22
+ // The flag is carried as a CycloneDX component `property` (name/value). Fall back
23
+ // to the legacy `cryptoProperties.harvestNowDecryptLater` location so an
24
+ // externally-authored / older CBOM still merges correctly.
25
+ const prop = c.properties?.find((p) => p.name === HNDL_PROP);
26
+ if (prop)
27
+ return prop.value === "true";
28
+ return (c.cryptoProperties
29
+ ?.harvestNowDecryptLater === true);
30
+ }
31
+ /** Return a copy of `props` with the HNDL flag set to `value`. */
32
+ function withHndl(props, value) {
33
+ const rest = (props ?? []).filter((p) => p.name !== HNDL_PROP);
34
+ return [...rest, { name: HNDL_PROP, value: String(value) }];
22
35
  }
23
36
  /** Union + dedup occurrences by `location`, sorted deterministically. */
24
37
  function mergeOccurrences(a, b) {
@@ -55,29 +68,38 @@ export function mergeCboms(boms) {
55
68
  const root = bom.metadata?.component?.name;
56
69
  if (typeof root === "string" && root && !roots.includes(root))
57
70
  roots.push(root);
58
- for (const c of bom.components) {
71
+ // `components` is optional in the CycloneDX 1.6 spec — a legal CBOM can carry none
72
+ // (a scan that found nothing). Treat a missing array as empty instead of crashing.
73
+ for (const c of bom.components ?? []) {
59
74
  const ref = c["bom-ref"];
60
75
  const existing = byRef.get(ref);
61
76
  if (!existing) {
62
- // Deep-ish copy so we can mutate evidence/hndl without touching the input.
77
+ // Deep-ish copy so we can mutate evidence/properties without touching the input.
63
78
  byRef.set(ref, {
64
79
  ...c,
65
80
  cryptoProperties: { ...c.cryptoProperties },
81
+ properties: c.properties ? [...c.properties] : undefined,
66
82
  evidence: { occurrences: occurrencesOf(c) },
67
83
  });
68
84
  }
69
85
  else {
70
86
  const merged = mergeOccurrences(occurrencesOf(existing), occurrencesOf(c));
71
87
  existing.evidence = { occurrences: merged };
72
- existing.cryptoProperties = {
73
- ...existing.cryptoProperties,
74
- harvestNowDecryptLater: hndlOf(existing) || hndlOf(c),
75
- };
88
+ // OR the harvest-now-decrypt-later flag across the merged copies.
89
+ existing.properties = withHndl(existing.properties, hndlOf(existing) || hndlOf(c));
76
90
  }
77
91
  }
78
92
  }
79
93
  const components = [...byRef.values()].sort((a, b) => a["bom-ref"] < b["bom-ref"] ? -1 : a["bom-ref"] > b["bom-ref"] ? 1 : 0);
80
- const serialSeed = components.map((c) => c["bom-ref"]).join("|") + `|${roots.join(",")}`;
94
+ // Content-address the serial over bom-refs AND their occurrence locations (+ roots),
95
+ // so two merges that differ only in occurrence evidence get distinct serials — a
96
+ // CycloneDX serialNumber is meant to identify a BOM *instance*, and downstream
97
+ // bom-link/dedup tooling relies on that. Occurrences are already sorted.
98
+ const serialSeed = components
99
+ .map((c) => `${c["bom-ref"]}#${occurrencesOf(c)
100
+ .map((o) => o.location)
101
+ .join(",")}`)
102
+ .join("|") + `|${roots.join(",")}`;
81
103
  const h = createHash("sha256").update(serialSeed, "utf8").digest("hex");
82
104
  const serial = `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
83
105
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"cbom-merge.js","sourceRoot":"","sources":["../src/cbom-merge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Merge multiple CycloneDX 1.6 CBOMs into one combined cryptographic bill of\n * materials. This is what turns the \"code + infra + live-endpoint\" story into a\n * single artifact: a qScan CBOM (code + infrastructure-as-code/config) and a\n * qProbe CBOM (live endpoints) both emit `cryptographic-asset` components keyed by\n * a deterministic `bom-ref` per (algorithm, primitive), so merging is a union with\n * dedup — the same crypto seen by two planes collapses to one component whose\n * occurrence evidence spans both.\n *\n * Output is deterministic (components sorted by bom-ref, occurrences sorted by\n * location, a stable serial derived from the merged content), so re-merging the\n * same inputs yields byte-identical output.\n */\nimport { createHash } from \"node:crypto\";\nimport type { CycloneDxBom, CbomComponent } from \"./cbom.js\";\n\n/** Occurrence record shape inside a component's `evidence.occurrences`. */\ninterface Occurrence {\n location: string;\n additionalContext?: string;\n}\n\nfunction occurrencesOf(c: CbomComponent): Occurrence[] {\n const ev = c.evidence as { occurrences?: unknown } | undefined;\n const occ = ev?.occurrences;\n return Array.isArray(occ) ? (occ as Occurrence[]) : [];\n}\n\nfunction hndlOf(c: CbomComponent): boolean {\n return (\n (c.cryptoProperties as { harvestNowDecryptLater?: unknown }).harvestNowDecryptLater === true\n );\n}\n\n/** Union + dedup occurrences by `location`, sorted deterministically. */\nfunction mergeOccurrences(a: Occurrence[], b: Occurrence[]): Occurrence[] {\n const byLoc = new Map<string, Occurrence>();\n for (const o of [...a, ...b]) if (!byLoc.has(o.location)) byLoc.set(o.location, o);\n return [...byLoc.values()].sort((x, y) =>\n x.location < y.location ? -1 : x.location > y.location ? 1 : 0,\n );\n}\n\n/**\n * Merge CBOMs into one. Components with the same `bom-ref` (same algorithm +\n * primitive) are combined; their occurrence evidence is unioned and their\n * harvest-now-decrypt-later flag is OR-ed. Tool and root metadata from every input\n * is preserved.\n */\nexport function mergeCboms(boms: readonly CycloneDxBom[]): CycloneDxBom {\n const byRef = new Map<string, CbomComponent>();\n const toolComponents: unknown[] = [];\n const toolSeen = new Set<string>();\n const roots: string[] = [];\n\n for (const bom of boms) {\n // Collect tool + root metadata (dedup tools by name@version).\n const tools = (bom.metadata as { tools?: { components?: unknown[] } })?.tools?.components;\n if (Array.isArray(tools)) {\n for (const t of tools) {\n const tc = t as { name?: string; version?: string };\n const key = `${tc.name}@${tc.version}`;\n if (!toolSeen.has(key)) {\n toolSeen.add(key);\n toolComponents.push(t);\n }\n }\n }\n const root = (bom.metadata as { component?: { name?: string } })?.component?.name;\n if (typeof root === \"string\" && root && !roots.includes(root)) roots.push(root);\n\n for (const c of bom.components) {\n const ref = c[\"bom-ref\"];\n const existing = byRef.get(ref);\n if (!existing) {\n // Deep-ish copy so we can mutate evidence/hndl without touching the input.\n byRef.set(ref, {\n ...c,\n cryptoProperties: { ...c.cryptoProperties },\n evidence: { occurrences: occurrencesOf(c) },\n });\n } else {\n const merged = mergeOccurrences(occurrencesOf(existing), occurrencesOf(c));\n existing.evidence = { occurrences: merged };\n existing.cryptoProperties = {\n ...existing.cryptoProperties,\n harvestNowDecryptLater: hndlOf(existing) || hndlOf(c),\n };\n }\n }\n }\n\n const components = [...byRef.values()].sort((a, b) =>\n a[\"bom-ref\"] < b[\"bom-ref\"] ? -1 : a[\"bom-ref\"] > b[\"bom-ref\"] ? 1 : 0,\n );\n\n const serialSeed = components.map((c) => c[\"bom-ref\"]).join(\"|\") + `|${roots.join(\",\")}`;\n const h = createHash(\"sha256\").update(serialSeed, \"utf8\").digest(\"hex\");\n const serial = `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;\n\n return {\n bomFormat: \"CycloneDX\",\n specVersion: \"1.6\",\n serialNumber: `urn:uuid:${serial}`,\n version: 1,\n metadata: {\n tools: { components: toolComponents },\n component: {\n type: \"application\",\n \"bom-ref\": \"root\",\n name: roots.length ? `combined: ${roots.join(\" + \")}` : \"combined\",\n },\n },\n components,\n };\n}\n"]}
1
+ {"version":3,"file":"cbom-merge.js","sourceRoot":"","sources":["../src/cbom-merge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Merge multiple CycloneDX 1.6 CBOMs into one combined cryptographic bill of\n * materials. This is what turns the \"code + infra + live-endpoint\" story into a\n * single artifact: a qScan CBOM (code + infrastructure-as-code/config) and a\n * qProbe CBOM (live endpoints) both emit `cryptographic-asset` components keyed by\n * a deterministic `bom-ref` per (algorithm, primitive), so merging is a union with\n * dedup — the same crypto seen by two planes collapses to one component whose\n * occurrence evidence spans both.\n *\n * Output is deterministic (components sorted by bom-ref, occurrences sorted by\n * location, a stable serial derived from the merged content), so re-merging the\n * same inputs yields byte-identical output.\n */\nimport { createHash } from \"node:crypto\";\nimport type { CycloneDxBom, CbomComponent } from \"./cbom.js\";\n\n/** Occurrence record shape inside a component's `evidence.occurrences`. */\ninterface Occurrence {\n location: string;\n additionalContext?: string;\n}\n\nfunction occurrencesOf(c: CbomComponent): Occurrence[] {\n const ev = c.evidence as { occurrences?: unknown } | undefined;\n const occ = ev?.occurrences;\n return Array.isArray(occ) ? (occ as Occurrence[]) : [];\n}\n\nconst HNDL_PROP = \"quantakrypto:harvestNowDecryptLater\";\n\nfunction hndlOf(c: CbomComponent): boolean {\n // The flag is carried as a CycloneDX component `property` (name/value). Fall back\n // to the legacy `cryptoProperties.harvestNowDecryptLater` location so an\n // externally-authored / older CBOM still merges correctly.\n const prop = c.properties?.find((p) => p.name === HNDL_PROP);\n if (prop) return prop.value === \"true\";\n return (\n (c.cryptoProperties as { harvestNowDecryptLater?: unknown } | undefined)\n ?.harvestNowDecryptLater === true\n );\n}\n\n/** Return a copy of `props` with the HNDL flag set to `value`. */\nfunction withHndl(\n props: { name: string; value: string }[] | undefined,\n value: boolean,\n): { name: string; value: string }[] {\n const rest = (props ?? []).filter((p) => p.name !== HNDL_PROP);\n return [...rest, { name: HNDL_PROP, value: String(value) }];\n}\n\n/** Union + dedup occurrences by `location`, sorted deterministically. */\nfunction mergeOccurrences(a: Occurrence[], b: Occurrence[]): Occurrence[] {\n const byLoc = new Map<string, Occurrence>();\n for (const o of [...a, ...b]) if (!byLoc.has(o.location)) byLoc.set(o.location, o);\n return [...byLoc.values()].sort((x, y) =>\n x.location < y.location ? -1 : x.location > y.location ? 1 : 0,\n );\n}\n\n/**\n * Merge CBOMs into one. Components with the same `bom-ref` (same algorithm +\n * primitive) are combined; their occurrence evidence is unioned and their\n * harvest-now-decrypt-later flag is OR-ed. Tool and root metadata from every input\n * is preserved.\n */\nexport function mergeCboms(boms: readonly CycloneDxBom[]): CycloneDxBom {\n const byRef = new Map<string, CbomComponent>();\n const toolComponents: unknown[] = [];\n const toolSeen = new Set<string>();\n const roots: string[] = [];\n\n for (const bom of boms) {\n // Collect tool + root metadata (dedup tools by name@version).\n const tools = (bom.metadata as { tools?: { components?: unknown[] } })?.tools?.components;\n if (Array.isArray(tools)) {\n for (const t of tools) {\n const tc = t as { name?: string; version?: string };\n const key = `${tc.name}@${tc.version}`;\n if (!toolSeen.has(key)) {\n toolSeen.add(key);\n toolComponents.push(t);\n }\n }\n }\n const root = (bom.metadata as { component?: { name?: string } })?.component?.name;\n if (typeof root === \"string\" && root && !roots.includes(root)) roots.push(root);\n\n // `components` is optional in the CycloneDX 1.6 spec — a legal CBOM can carry none\n // (a scan that found nothing). Treat a missing array as empty instead of crashing.\n for (const c of bom.components ?? []) {\n const ref = c[\"bom-ref\"];\n const existing = byRef.get(ref);\n if (!existing) {\n // Deep-ish copy so we can mutate evidence/properties without touching the input.\n byRef.set(ref, {\n ...c,\n cryptoProperties: { ...c.cryptoProperties },\n properties: c.properties ? [...c.properties] : undefined,\n evidence: { occurrences: occurrencesOf(c) },\n });\n } else {\n const merged = mergeOccurrences(occurrencesOf(existing), occurrencesOf(c));\n existing.evidence = { occurrences: merged };\n // OR the harvest-now-decrypt-later flag across the merged copies.\n existing.properties = withHndl(existing.properties, hndlOf(existing) || hndlOf(c));\n }\n }\n }\n\n const components = [...byRef.values()].sort((a, b) =>\n a[\"bom-ref\"] < b[\"bom-ref\"] ? -1 : a[\"bom-ref\"] > b[\"bom-ref\"] ? 1 : 0,\n );\n\n // Content-address the serial over bom-refs AND their occurrence locations (+ roots),\n // so two merges that differ only in occurrence evidence get distinct serials — a\n // CycloneDX serialNumber is meant to identify a BOM *instance*, and downstream\n // bom-link/dedup tooling relies on that. Occurrences are already sorted.\n const serialSeed =\n components\n .map(\n (c) =>\n `${c[\"bom-ref\"]}#${occurrencesOf(c)\n .map((o) => o.location)\n .join(\",\")}`,\n )\n .join(\"|\") + `|${roots.join(\",\")}`;\n const h = createHash(\"sha256\").update(serialSeed, \"utf8\").digest(\"hex\");\n const serial = `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;\n\n return {\n bomFormat: \"CycloneDX\",\n specVersion: \"1.6\",\n serialNumber: `urn:uuid:${serial}`,\n version: 1,\n metadata: {\n tools: { components: toolComponents },\n component: {\n type: \"application\",\n \"bom-ref\": \"root\",\n name: roots.length ? `combined: ${roots.join(\" + \")}` : \"combined\",\n },\n },\n components,\n };\n}\n"]}
package/dist/cbom.d.ts CHANGED
@@ -14,6 +14,11 @@ export interface CbomComponent {
14
14
  "bom-ref": string;
15
15
  name: string;
16
16
  cryptoProperties: Record<string, unknown>;
17
+ /** CycloneDX component name/value properties (carries the quantum posture flags). */
18
+ properties?: {
19
+ name: string;
20
+ value: string;
21
+ }[];
17
22
  evidence?: Record<string, unknown>;
18
23
  }
19
24
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cbom.d.ts","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAA6C,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAC;IACvB,WAAW,EAAE,KAAK,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAiED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,CAqFvD"}
1
+ {"version":3,"file":"cbom.d.ts","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAA6C,UAAU,EAAE,MAAM,YAAY,CAAC;AAGxF,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,WAAW,CAAC;IACvB,WAAW,EAAE,KAAK,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,qFAAqF;IACrF,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAwHD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,CAqIvD"}
package/dist/cbom.js CHANGED
@@ -5,13 +5,16 @@
5
5
  * for compliance / supply-chain tooling.
6
6
  *
7
7
  * Reference: CycloneDX 1.6 cryptography properties
8
- * (https://cyclonedx.org/capabilities/cbom/). We emit one
9
- * `cryptographic-asset` component per distinct (algorithm, primitive) pair
10
- * observed, with occurrence evidence pointing back at the findings.
8
+ * (https://cyclonedx.org/capabilities/cbom/). Each finding is classified into
9
+ * its CycloneDX `assetType` `algorithm` (usage), `certificate` (X.509),
10
+ * `related-crypto-material` (private/public key material), or `protocol` (TLS)
11
+ * and we emit one `cryptographic-asset` component per distinct
12
+ * (assetType, algorithm, discriminator), with occurrence evidence pointing back
13
+ * at the findings. Every asset carries the quantum posture flags.
11
14
  */
12
15
  import { createHash } from "node:crypto";
13
16
  import { VERSION } from "./version.js";
14
- /** CycloneDX cryptographic primitive for a finding category. */
17
+ /** CycloneDX cryptographic primitive for a finding category (algorithm assets only). */
15
18
  function primitiveFor(category) {
16
19
  switch (category) {
17
20
  case "kem":
@@ -20,17 +23,63 @@ function primitiveFor(category) {
20
23
  return "key-agree";
21
24
  case "signature":
22
25
  return "signature";
23
- case "certificate":
24
- // "pki" is NOT a valid CycloneDX 1.6 algorithmProperties.primitive enum
25
- // value; use "other" so the CBOM validates (audit: quantum #3). Modeling
26
- // certificates as assetType:"certificate" is a future refinement.
27
- return "other";
28
- case "tls":
29
- return "other";
26
+ case "hash":
27
+ return "hash";
30
28
  default:
29
+ // `certificate` / `tls` no longer route here (they become their own
30
+ // assetType — see {@link classifyAsset}); `dependency` / `rng` and any
31
+ // future category fall back to the valid "other" primitive.
31
32
  return "other";
32
33
  }
33
34
  }
35
+ /**
36
+ * Classify a finding into its CycloneDX 1.6 asset type. Most findings are
37
+ * algorithm USAGE (`kem` / `key-exchange` / `signature` / `hash` / …). The
38
+ * `certificate` category is a mix of real X.509 certificates and raw key
39
+ * material, disambiguated by rule id; `tls` findings are protocol configuration.
40
+ *
41
+ * `discriminator` keeps distinct kinds in separate components (it is the algorithm
42
+ * primitive for `algorithm`, the material type for `related-crypto-material`, the
43
+ * protocol type for `protocol`, and empty for `certificate`).
44
+ */
45
+ function classifyAsset(f) {
46
+ if (f.category === "tls") {
47
+ return { assetType: "protocol", discriminator: "tls", protocolType: "tls" };
48
+ }
49
+ if (f.category === "certificate") {
50
+ const id = f.ruleId.toLowerCase();
51
+ // Route the explicitly key-SHAPED material to related-crypto-material…
52
+ if (id.includes("private-key") || id.includes("keystore")) {
53
+ return {
54
+ assetType: "related-crypto-material",
55
+ discriminator: "private-key",
56
+ materialType: "private-key",
57
+ };
58
+ }
59
+ if (id.includes("public-key")) {
60
+ return {
61
+ assetType: "related-crypto-material",
62
+ discriminator: "public-key",
63
+ materialType: "public-key",
64
+ };
65
+ }
66
+ if (id.includes("message")) {
67
+ return {
68
+ assetType: "related-crypto-material",
69
+ discriminator: "ciphertext",
70
+ materialType: "ciphertext",
71
+ };
72
+ }
73
+ // …and DEFAULT the rest of the `certificate` category to assetType
74
+ // "certificate" — the category's own meaning. This covers real X.509 certs
75
+ // and CSRs (pem-certificate/pem-cert-request) plus PKI trust material whose
76
+ // ids don't spell "cert": ACM certs (cfn-acm-*), Vault PKI (vault-pki-*),
77
+ // mesh identity, and SPIFFE X.509-SVIDs (spire-*). Previously these fell
78
+ // through to related-crypto-material "key", mislabelling certificates.
79
+ return { assetType: "certificate", discriminator: "" };
80
+ }
81
+ return { assetType: "algorithm", discriminator: primitiveFor(f.category) };
82
+ }
34
83
  /**
35
84
  * Every CBOM asset here is derived from a quantakrypto finding, and the detectors
36
85
  * only ever fire on classical (Shor-broken) public-key crypto — so an asset is
@@ -76,15 +125,17 @@ function bomRef(key) {
76
125
  * finding. Output is deterministic (components and occurrences are sorted).
77
126
  */
78
127
  export function toCbom(result) {
79
- // Group findings by (algorithm | primitive).
128
+ // Group findings by (assetType | algorithm | discriminator) so each distinct
129
+ // cryptographic asset — an algorithm usage, a certificate, key material, or a
130
+ // protocol — becomes its own component.
80
131
  const groups = new Map();
81
132
  for (const f of result.findings) {
82
133
  const algorithm = f.algorithm ?? "unknown";
83
- const primitive = primitiveFor(f.category);
84
- const key = `${algorithm}|${primitive}`;
134
+ const cls = classifyAsset(f);
135
+ const key = `${cls.assetType}|${algorithm}|${cls.discriminator}`;
85
136
  let g = groups.get(key);
86
137
  if (!g) {
87
- g = { algorithm, primitive, findings: [] };
138
+ g = { ...cls, algorithm, findings: [] };
88
139
  groups.set(key, g);
89
140
  }
90
141
  g.findings.push(f);
@@ -99,29 +150,67 @@ export function toCbom(result) {
99
150
  }))
100
151
  .sort((a, b) => (a.location < b.location ? -1 : a.location > b.location ? 1 : 0));
101
152
  const anyHndl = g.findings.some((f) => f.hndl);
153
+ // The type-specific CycloneDX properties block + a human label for the name.
154
+ let typeProps;
155
+ let label;
156
+ switch (g.assetType) {
157
+ case "certificate":
158
+ // A certificate / CSR: we know the public-key family but not the X.509
159
+ // fields (subject, validity, refs) from a lexical scan, so the block is
160
+ // left minimal — all certificateProperties are optional in 1.6.
161
+ typeProps = { certificateProperties: {} };
162
+ label = "certificate";
163
+ break;
164
+ case "related-crypto-material":
165
+ typeProps = {
166
+ relatedCryptoMaterialProperties: { type: g.materialType ?? "key" },
167
+ };
168
+ label = g.materialType ?? "key";
169
+ break;
170
+ case "protocol":
171
+ typeProps = { protocolProperties: { type: g.protocolType ?? "tls" } };
172
+ label = g.protocolType ?? "tls";
173
+ break;
174
+ case "algorithm":
175
+ default:
176
+ typeProps = {
177
+ algorithmProperties: {
178
+ primitive: g.discriminator,
179
+ parameterSetIdentifier: g.algorithm,
180
+ executionEnvironment: "software-plain-ram",
181
+ classicalSecurityLevel: classicalSecurityLevelFor(g.algorithm),
182
+ nistQuantumSecurityLevel: 0,
183
+ cryptoFunctions: g.discriminator === "signature"
184
+ ? ["sign", "verify"]
185
+ : g.discriminator === "kem"
186
+ ? ["encapsulate", "decapsulate"]
187
+ : // CycloneDX 1.6 has no "keyagree" cryptoFunction; "other" is
188
+ // the valid value for a key-agreement primitive.
189
+ ["other"],
190
+ },
191
+ };
192
+ label = g.discriminator;
193
+ break;
194
+ }
102
195
  return {
103
196
  type: "cryptographic-asset",
104
197
  "bom-ref": bomRef(key),
105
- name: `${g.algorithm} (${g.primitive})`,
198
+ name: `${g.algorithm} (${label})`,
106
199
  cryptoProperties: {
107
- assetType: "algorithm",
108
- algorithmProperties: {
109
- primitive: g.primitive,
110
- parameterSetIdentifier: g.algorithm,
111
- executionEnvironment: "software-plain-ram",
112
- classicalSecurityLevel: classicalSecurityLevelFor(g.algorithm),
113
- nistQuantumSecurityLevel: 0,
114
- cryptoFunctions: g.primitive === "signature"
115
- ? ["sign", "verify"]
116
- : g.primitive === "kem"
117
- ? ["encapsulate", "decapsulate"]
118
- : g.primitive === "key-agree"
119
- ? ["keyagree"]
120
- : ["other"],
121
- },
122
- quantumVulnerable: isQuantumVulnerable(g.algorithm),
123
- harvestNowDecryptLater: anyHndl,
200
+ assetType: g.assetType,
201
+ ...typeProps,
124
202
  },
203
+ // The quantum posture flags are carried as CycloneDX component `properties`
204
+ // (an open name/value list) rather than inside `cryptoProperties`, whose
205
+ // 1.6 schema is `additionalProperties: false` — so a strict validator
206
+ // accepts the BOM. Namespaced to avoid clashing with other tools' keys.
207
+ properties: [
208
+ {
209
+ name: "quantakrypto:quantumVulnerable",
210
+ value: String(isQuantumVulnerable(g.algorithm)),
211
+ },
212
+ { name: "quantakrypto:harvestNowDecryptLater", value: String(anyHndl) },
213
+ ],
125
214
  evidence: { occurrences },
126
215
  };
127
216
  });
@@ -153,12 +242,18 @@ export function toCbom(result) {
153
242
  }
154
243
  /**
155
244
  * Derive a stable UUID-shaped serial from the scan result so re-exporting the
156
- * same result yields the same serial number (deterministic output).
245
+ * same result yields the same serial number (deterministic output). Hashed over the
246
+ * actual finding set (ruleId + location per finding), NOT just the COUNT — two scans
247
+ * with the same number of totally different findings must not collide, since a
248
+ * CycloneDX serialNumber identifies a specific BOM instance. Findings are already in
249
+ * a stable order (scan.ts sorts by file/line/ruleId).
157
250
  */
158
251
  function stableUuid(result) {
159
- const h = createHash("sha256")
160
- .update(`${result.root}|${result.toolVersion}|${result.findings.length}`, "utf8")
161
- .digest("hex");
252
+ const hash = createHash("sha256").update(`${result.root}|${result.toolVersion}`, "utf8");
253
+ for (const f of result.findings) {
254
+ hash.update(`\n${f.ruleId}@${f.location.file}:${f.location.line}:${f.location.column ?? 0}`);
255
+ }
256
+ const h = hash.digest("hex");
162
257
  // Shape as a v4-ish UUID (variant/version nibbles forced).
163
258
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
164
259
  }
package/dist/cbom.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cbom.js","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * CBOM (Cryptographic Bill of Materials) export — CycloneDX 1.6 with\n * `cryptographic-asset` components. Turns a {@link ScanResult} into a\n * machine-readable inventory of the classical cryptographic assets discovered,\n * for compliance / supply-chain tooling.\n *\n * Reference: CycloneDX 1.6 cryptography properties\n * (https://cyclonedx.org/capabilities/cbom/). We emit one\n * `cryptographic-asset` component per distinct (algorithm, primitive) pair\n * observed, with occurrence evidence pointing back at the findings.\n */\nimport { createHash } from \"node:crypto\";\n\nimport type { AlgorithmFamily, Finding, FindingCategory, ScanResult } from \"./types.js\";\nimport { VERSION } from \"./version.js\";\n\n/** A CycloneDX 1.6 cryptographic bill of materials (kept permissive). */\nexport interface CycloneDxBom {\n bomFormat: \"CycloneDX\";\n specVersion: \"1.6\";\n serialNumber: string;\n version: number;\n metadata: Record<string, unknown>;\n components: CbomComponent[];\n}\n\n/** A single CycloneDX `cryptographic-asset` component. */\nexport interface CbomComponent {\n type: \"cryptographic-asset\";\n \"bom-ref\": string;\n name: string;\n cryptoProperties: Record<string, unknown>;\n evidence?: Record<string, unknown>;\n}\n\n/** CycloneDX cryptographic primitive for a finding category. */\nfunction primitiveFor(category: FindingCategory): string {\n switch (category) {\n case \"kem\":\n return \"kem\";\n case \"key-exchange\":\n return \"key-agree\";\n case \"signature\":\n return \"signature\";\n case \"certificate\":\n // \"pki\" is NOT a valid CycloneDX 1.6 algorithmProperties.primitive enum\n // value; use \"other\" so the CBOM validates (audit: quantum #3). Modeling\n // certificates as assetType:\"certificate\" is a future refinement.\n return \"other\";\n case \"tls\":\n return \"other\";\n default:\n return \"other\";\n }\n}\n\n/**\n * Every CBOM asset here is derived from a quantakrypto finding, and the detectors\n * only ever fire on classical (Shor-broken) public-key crypto — so an asset is\n * quantum-vulnerable by construction, even when the exact family couldn't be\n * pinned down (`unknown`). Reporting `false` for `unknown` mislabeled\n * definitionally-classical findings as safe (audit: crypto #6).\n */\nfunction isQuantumVulnerable(_algorithm: AlgorithmFamily): boolean {\n return true;\n}\n\n/**\n * Approximate CLASSICAL security strength (bits) of the common parameterisation\n * of a classical family — RSA-2048 / DH-2048 ≈ 112-bit, 256-bit curves ≈ 128-bit,\n * X448 ≈ 224-bit. (The QUANTUM level is 0 for all of these: Shor breaks them.)\n * 0 when the family is unknown. Reported so a CBOM doesn't imply these primitives\n * have zero classical strength today.\n */\nfunction classicalSecurityLevelFor(algorithm: AlgorithmFamily): number {\n switch (algorithm) {\n case \"RSA\":\n case \"DH\":\n case \"DSA\":\n return 112;\n case \"ECDH\":\n case \"ECDSA\":\n case \"EdDSA\":\n case \"X25519\":\n case \"ECIES\":\n return 128;\n case \"X448\":\n return 224;\n default:\n return 0;\n }\n}\n\n/** Deterministic bom-ref for a (algorithm, primitive) asset key. */\nfunction bomRef(key: string): string {\n return `crypto:${createHash(\"sha256\").update(key, \"utf8\").digest(\"hex\").slice(0, 16)}`;\n}\n\n/**\n * Build a CycloneDX 1.6 CBOM from a scan result. One component per distinct\n * (algorithm + primitive) pair, with occurrence evidence (file:line) per\n * finding. Output is deterministic (components and occurrences are sorted).\n */\nexport function toCbom(result: ScanResult): CycloneDxBom {\n // Group findings by (algorithm | primitive).\n const groups = new Map<\n string,\n { algorithm: AlgorithmFamily; primitive: string; findings: Finding[] }\n >();\n\n for (const f of result.findings) {\n const algorithm: AlgorithmFamily = f.algorithm ?? \"unknown\";\n const primitive = primitiveFor(f.category);\n const key = `${algorithm}|${primitive}`;\n let g = groups.get(key);\n if (!g) {\n g = { algorithm, primitive, findings: [] };\n groups.set(key, g);\n }\n g.findings.push(f);\n }\n\n const components: CbomComponent[] = [...groups.entries()]\n .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))\n .map(([key, g]) => {\n const occurrences = g.findings\n .map((f) => ({\n location: `${f.location.file}:${f.location.line}`,\n ...(f.cwe ? { additionalContext: f.cwe } : {}),\n }))\n .sort((a, b) => (a.location < b.location ? -1 : a.location > b.location ? 1 : 0));\n\n const anyHndl = g.findings.some((f) => f.hndl);\n\n return {\n type: \"cryptographic-asset\" as const,\n \"bom-ref\": bomRef(key),\n name: `${g.algorithm} (${g.primitive})`,\n cryptoProperties: {\n assetType: \"algorithm\",\n algorithmProperties: {\n primitive: g.primitive,\n parameterSetIdentifier: g.algorithm,\n executionEnvironment: \"software-plain-ram\",\n classicalSecurityLevel: classicalSecurityLevelFor(g.algorithm),\n nistQuantumSecurityLevel: 0,\n cryptoFunctions:\n g.primitive === \"signature\"\n ? [\"sign\", \"verify\"]\n : g.primitive === \"kem\"\n ? [\"encapsulate\", \"decapsulate\"]\n : g.primitive === \"key-agree\"\n ? [\"keyagree\"]\n : [\"other\"],\n },\n quantumVulnerable: isQuantumVulnerable(g.algorithm),\n harvestNowDecryptLater: anyHndl,\n },\n evidence: { occurrences },\n };\n });\n\n const serial = `urn:uuid:${stableUuid(result)}`;\n\n return {\n bomFormat: \"CycloneDX\",\n specVersion: \"1.6\",\n serialNumber: serial,\n version: 1,\n metadata: {\n timestamp: result.finishedAt,\n tools: {\n components: [\n {\n type: \"application\",\n name: \"qScan\",\n version: result.toolVersion || VERSION,\n },\n ],\n },\n component: {\n type: \"application\",\n \"bom-ref\": \"root\",\n name: result.root,\n },\n },\n components,\n };\n}\n\n/**\n * Derive a stable UUID-shaped serial from the scan result so re-exporting the\n * same result yields the same serial number (deterministic output).\n */\nfunction stableUuid(result: ScanResult): string {\n const h = createHash(\"sha256\")\n .update(`${result.root}|${result.toolVersion}|${result.findings.length}`, \"utf8\")\n .digest(\"hex\");\n // Shape as a v4-ish UUID (variant/version nibbles forced).\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;\n}\n"]}
1
+ {"version":3,"file":"cbom.js","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAuBvC,wFAAwF;AACxF,SAAS,YAAY,CAAC,QAAyB;IAC7C,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;QACf,KAAK,cAAc;YACjB,OAAO,WAAW,CAAC;QACrB,KAAK,WAAW;YACd,OAAO,WAAW,CAAC;QACrB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB;YACE,oEAAoE;YACpE,uEAAuE;YACvE,4DAA4D;YAC5D,OAAO,OAAO,CAAC;IACnB,CAAC;AACH,CAAC;AAKD;;;;;;;;;GASG;AACH,SAAS,aAAa,CAAC,CAAU;IAM/B,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;QACzB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC9E,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAClC,uEAAuE;QACvE,IAAI,EAAE,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1D,OAAO;gBACL,SAAS,EAAE,yBAAyB;gBACpC,aAAa,EAAE,aAAa;gBAC5B,YAAY,EAAE,aAAa;aAC5B,CAAC;QACJ,CAAC;QACD,IAAI,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9B,OAAO;gBACL,SAAS,EAAE,yBAAyB;gBACpC,aAAa,EAAE,YAAY;gBAC3B,YAAY,EAAE,YAAY;aAC3B,CAAC;QACJ,CAAC;QACD,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,SAAS,EAAE,yBAAyB;gBACpC,aAAa,EAAE,YAAY;gBAC3B,YAAY,EAAE,YAAY;aAC3B,CAAC;QACJ,CAAC;QACD,mEAAmE;QACnE,2EAA2E;QAC3E,4EAA4E;QAC5E,0EAA0E;QAC1E,yEAAyE;QACzE,uEAAuE;QACvE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;IACzD,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC7E,CAAC;AAED;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,UAA2B;IACtD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,SAAS,yBAAyB,CAAC,SAA0B;IAC3D,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,KAAK,CAAC;QACX,KAAK,IAAI,CAAC;QACV,KAAK,KAAK;YACR,OAAO,GAAG,CAAC;QACb,KAAK,MAAM,CAAC;QACZ,KAAK,OAAO,CAAC;QACb,KAAK,OAAO,CAAC;QACb,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO;YACV,OAAO,GAAG,CAAC;QACb,KAAK,MAAM;YACT,OAAO,GAAG,CAAC;QACb;YACE,OAAO,CAAC,CAAC;IACb,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,SAAS,MAAM,CAAC,GAAW;IACzB,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACzF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,MAAkB;IACvC,6EAA6E;IAC7E,8EAA8E;IAC9E,wCAAwC;IACxC,MAAM,MAAM,GAAG,IAAI,GAAG,EAUnB,CAAC;IAEJ,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,SAAS,GAAoB,CAAC,CAAC,SAAS,IAAI,SAAS,CAAC;QAC5D,MAAM,GAAG,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,SAAS,IAAI,SAAS,IAAI,GAAG,CAAC,aAAa,EAAE,CAAC;QACjE,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;YACxC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,CAAC;QACD,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IAED,MAAM,UAAU,GAAoB,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACtD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE;QAChB,MAAM,WAAW,GAAG,CAAC,CAAC,QAAQ;aAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,QAAQ,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE;YACjD,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC/C,CAAC,CAAC;aACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAEpF,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAE/C,6EAA6E;QAC7E,IAAI,SAAkC,CAAC;QACvC,IAAI,KAAa,CAAC;QAClB,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC;YACpB,KAAK,aAAa;gBAChB,uEAAuE;gBACvE,wEAAwE;gBACxE,gEAAgE;gBAChE,SAAS,GAAG,EAAE,qBAAqB,EAAE,EAAE,EAAE,CAAC;gBAC1C,KAAK,GAAG,aAAa,CAAC;gBACtB,MAAM;YACR,KAAK,yBAAyB;gBAC5B,SAAS,GAAG;oBACV,+BAA+B,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,IAAI,KAAK,EAAE;iBACnE,CAAC;gBACF,KAAK,GAAG,CAAC,CAAC,YAAY,IAAI,KAAK,CAAC;gBAChC,MAAM;YACR,KAAK,UAAU;gBACb,SAAS,GAAG,EAAE,kBAAkB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,IAAI,KAAK,EAAE,EAAE,CAAC;gBACtE,KAAK,GAAG,CAAC,CAAC,YAAY,IAAI,KAAK,CAAC;gBAChC,MAAM;YACR,KAAK,WAAW,CAAC;YACjB;gBACE,SAAS,GAAG;oBACV,mBAAmB,EAAE;wBACnB,SAAS,EAAE,CAAC,CAAC,aAAa;wBAC1B,sBAAsB,EAAE,CAAC,CAAC,SAAS;wBACnC,oBAAoB,EAAE,oBAAoB;wBAC1C,sBAAsB,EAAE,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC;wBAC9D,wBAAwB,EAAE,CAAC;wBAC3B,eAAe,EACb,CAAC,CAAC,aAAa,KAAK,WAAW;4BAC7B,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC;4BACpB,CAAC,CAAC,CAAC,CAAC,aAAa,KAAK,KAAK;gCACzB,CAAC,CAAC,CAAC,aAAa,EAAE,aAAa,CAAC;gCAChC,CAAC,CAAC,6DAA6D;oCAC7D,iDAAiD;oCACjD,CAAC,OAAO,CAAC;qBAClB;iBACF,CAAC;gBACF,KAAK,GAAG,CAAC,CAAC,aAAa,CAAC;gBACxB,MAAM;QACV,CAAC;QAED,OAAO;YACL,IAAI,EAAE,qBAA8B;YACpC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC;YACtB,IAAI,EAAE,GAAG,CAAC,CAAC,SAAS,KAAK,KAAK,GAAG;YACjC,gBAAgB,EAAE;gBAChB,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,GAAG,SAAS;aACb;YACD,4EAA4E;YAC5E,yEAAyE;YACzE,sEAAsE;YACtE,wEAAwE;YACxE,UAAU,EAAE;gBACV;oBACE,IAAI,EAAE,gCAAgC;oBACtC,KAAK,EAAE,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;iBAChD;gBACD,EAAE,IAAI,EAAE,qCAAqC,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE;aACxE;YACD,QAAQ,EAAE,EAAE,WAAW,EAAE;SAC1B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEL,MAAM,MAAM,GAAG,YAAY,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;IAEhD,OAAO;QACL,SAAS,EAAE,WAAW;QACtB,WAAW,EAAE,KAAK;QAClB,YAAY,EAAE,MAAM;QACpB,OAAO,EAAE,CAAC;QACV,QAAQ,EAAE;YACR,SAAS,EAAE,MAAM,CAAC,UAAU;YAC5B,KAAK,EAAE;gBACL,UAAU,EAAE;oBACV;wBACE,IAAI,EAAE,aAAa;wBACnB,IAAI,EAAE,OAAO;wBACb,OAAO,EAAE,MAAM,CAAC,WAAW,IAAI,OAAO;qBACvC;iBACF;aACF;YACD,SAAS,EAAE;gBACT,IAAI,EAAE,aAAa;gBACnB,SAAS,EAAE,MAAM;gBACjB,IAAI,EAAE,MAAM,CAAC,IAAI;aAClB;SACF;QACD,UAAU;KACX,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,MAAkB;IACpC,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,WAAW,EAAE,EAAE,MAAM,CAAC,CAAC;IACzF,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/F,CAAC;IACD,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,2DAA2D;IAC3D,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;AACzG,CAAC","sourcesContent":["/**\n * CBOM (Cryptographic Bill of Materials) export — CycloneDX 1.6 with\n * `cryptographic-asset` components. Turns a {@link ScanResult} into a\n * machine-readable inventory of the classical cryptographic assets discovered,\n * for compliance / supply-chain tooling.\n *\n * Reference: CycloneDX 1.6 cryptography properties\n * (https://cyclonedx.org/capabilities/cbom/). Each finding is classified into\n * its CycloneDX `assetType` — `algorithm` (usage), `certificate` (X.509),\n * `related-crypto-material` (private/public key material), or `protocol` (TLS) —\n * and we emit one `cryptographic-asset` component per distinct\n * (assetType, algorithm, discriminator), with occurrence evidence pointing back\n * at the findings. Every asset carries the quantum posture flags.\n */\nimport { createHash } from \"node:crypto\";\n\nimport type { AlgorithmFamily, Finding, FindingCategory, ScanResult } from \"./types.js\";\nimport { VERSION } from \"./version.js\";\n\n/** A CycloneDX 1.6 cryptographic bill of materials (kept permissive). */\nexport interface CycloneDxBom {\n bomFormat: \"CycloneDX\";\n specVersion: \"1.6\";\n serialNumber: string;\n version: number;\n metadata: Record<string, unknown>;\n components: CbomComponent[];\n}\n\n/** A single CycloneDX `cryptographic-asset` component. */\nexport interface CbomComponent {\n type: \"cryptographic-asset\";\n \"bom-ref\": string;\n name: string;\n cryptoProperties: Record<string, unknown>;\n /** CycloneDX component name/value properties (carries the quantum posture flags). */\n properties?: { name: string; value: string }[];\n evidence?: Record<string, unknown>;\n}\n\n/** CycloneDX cryptographic primitive for a finding category (algorithm assets only). */\nfunction primitiveFor(category: FindingCategory): string {\n switch (category) {\n case \"kem\":\n return \"kem\";\n case \"key-exchange\":\n return \"key-agree\";\n case \"signature\":\n return \"signature\";\n case \"hash\":\n return \"hash\";\n default:\n // `certificate` / `tls` no longer route here (they become their own\n // assetType — see {@link classifyAsset}); `dependency` / `rng` and any\n // future category fall back to the valid \"other\" primitive.\n return \"other\";\n }\n}\n\n/** The CycloneDX 1.6 `cryptoProperties.assetType` a finding maps to. */\ntype CbomAssetType = \"algorithm\" | \"certificate\" | \"protocol\" | \"related-crypto-material\";\n\n/**\n * Classify a finding into its CycloneDX 1.6 asset type. Most findings are\n * algorithm USAGE (`kem` / `key-exchange` / `signature` / `hash` / …). The\n * `certificate` category is a mix of real X.509 certificates and raw key\n * material, disambiguated by rule id; `tls` findings are protocol configuration.\n *\n * `discriminator` keeps distinct kinds in separate components (it is the algorithm\n * primitive for `algorithm`, the material type for `related-crypto-material`, the\n * protocol type for `protocol`, and empty for `certificate`).\n */\nfunction classifyAsset(f: Finding): {\n assetType: CbomAssetType;\n discriminator: string;\n materialType?: string;\n protocolType?: string;\n} {\n if (f.category === \"tls\") {\n return { assetType: \"protocol\", discriminator: \"tls\", protocolType: \"tls\" };\n }\n if (f.category === \"certificate\") {\n const id = f.ruleId.toLowerCase();\n // Route the explicitly key-SHAPED material to related-crypto-material…\n if (id.includes(\"private-key\") || id.includes(\"keystore\")) {\n return {\n assetType: \"related-crypto-material\",\n discriminator: \"private-key\",\n materialType: \"private-key\",\n };\n }\n if (id.includes(\"public-key\")) {\n return {\n assetType: \"related-crypto-material\",\n discriminator: \"public-key\",\n materialType: \"public-key\",\n };\n }\n if (id.includes(\"message\")) {\n return {\n assetType: \"related-crypto-material\",\n discriminator: \"ciphertext\",\n materialType: \"ciphertext\",\n };\n }\n // …and DEFAULT the rest of the `certificate` category to assetType\n // \"certificate\" — the category's own meaning. This covers real X.509 certs\n // and CSRs (pem-certificate/pem-cert-request) plus PKI trust material whose\n // ids don't spell \"cert\": ACM certs (cfn-acm-*), Vault PKI (vault-pki-*),\n // mesh identity, and SPIFFE X.509-SVIDs (spire-*). Previously these fell\n // through to related-crypto-material \"key\", mislabelling certificates.\n return { assetType: \"certificate\", discriminator: \"\" };\n }\n return { assetType: \"algorithm\", discriminator: primitiveFor(f.category) };\n}\n\n/**\n * Every CBOM asset here is derived from a quantakrypto finding, and the detectors\n * only ever fire on classical (Shor-broken) public-key crypto — so an asset is\n * quantum-vulnerable by construction, even when the exact family couldn't be\n * pinned down (`unknown`). Reporting `false` for `unknown` mislabeled\n * definitionally-classical findings as safe (audit: crypto #6).\n */\nfunction isQuantumVulnerable(_algorithm: AlgorithmFamily): boolean {\n return true;\n}\n\n/**\n * Approximate CLASSICAL security strength (bits) of the common parameterisation\n * of a classical family — RSA-2048 / DH-2048 ≈ 112-bit, 256-bit curves ≈ 128-bit,\n * X448 ≈ 224-bit. (The QUANTUM level is 0 for all of these: Shor breaks them.)\n * 0 when the family is unknown. Reported so a CBOM doesn't imply these primitives\n * have zero classical strength today.\n */\nfunction classicalSecurityLevelFor(algorithm: AlgorithmFamily): number {\n switch (algorithm) {\n case \"RSA\":\n case \"DH\":\n case \"DSA\":\n return 112;\n case \"ECDH\":\n case \"ECDSA\":\n case \"EdDSA\":\n case \"X25519\":\n case \"ECIES\":\n return 128;\n case \"X448\":\n return 224;\n default:\n return 0;\n }\n}\n\n/** Deterministic bom-ref for a (algorithm, primitive) asset key. */\nfunction bomRef(key: string): string {\n return `crypto:${createHash(\"sha256\").update(key, \"utf8\").digest(\"hex\").slice(0, 16)}`;\n}\n\n/**\n * Build a CycloneDX 1.6 CBOM from a scan result. One component per distinct\n * (algorithm + primitive) pair, with occurrence evidence (file:line) per\n * finding. Output is deterministic (components and occurrences are sorted).\n */\nexport function toCbom(result: ScanResult): CycloneDxBom {\n // Group findings by (assetType | algorithm | discriminator) so each distinct\n // cryptographic asset — an algorithm usage, a certificate, key material, or a\n // protocol — becomes its own component.\n const groups = new Map<\n string,\n {\n assetType: CbomAssetType;\n algorithm: AlgorithmFamily;\n discriminator: string;\n materialType?: string;\n protocolType?: string;\n findings: Finding[];\n }\n >();\n\n for (const f of result.findings) {\n const algorithm: AlgorithmFamily = f.algorithm ?? \"unknown\";\n const cls = classifyAsset(f);\n const key = `${cls.assetType}|${algorithm}|${cls.discriminator}`;\n let g = groups.get(key);\n if (!g) {\n g = { ...cls, algorithm, findings: [] };\n groups.set(key, g);\n }\n g.findings.push(f);\n }\n\n const components: CbomComponent[] = [...groups.entries()]\n .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))\n .map(([key, g]) => {\n const occurrences = g.findings\n .map((f) => ({\n location: `${f.location.file}:${f.location.line}`,\n ...(f.cwe ? { additionalContext: f.cwe } : {}),\n }))\n .sort((a, b) => (a.location < b.location ? -1 : a.location > b.location ? 1 : 0));\n\n const anyHndl = g.findings.some((f) => f.hndl);\n\n // The type-specific CycloneDX properties block + a human label for the name.\n let typeProps: Record<string, unknown>;\n let label: string;\n switch (g.assetType) {\n case \"certificate\":\n // A certificate / CSR: we know the public-key family but not the X.509\n // fields (subject, validity, refs) from a lexical scan, so the block is\n // left minimal — all certificateProperties are optional in 1.6.\n typeProps = { certificateProperties: {} };\n label = \"certificate\";\n break;\n case \"related-crypto-material\":\n typeProps = {\n relatedCryptoMaterialProperties: { type: g.materialType ?? \"key\" },\n };\n label = g.materialType ?? \"key\";\n break;\n case \"protocol\":\n typeProps = { protocolProperties: { type: g.protocolType ?? \"tls\" } };\n label = g.protocolType ?? \"tls\";\n break;\n case \"algorithm\":\n default:\n typeProps = {\n algorithmProperties: {\n primitive: g.discriminator,\n parameterSetIdentifier: g.algorithm,\n executionEnvironment: \"software-plain-ram\",\n classicalSecurityLevel: classicalSecurityLevelFor(g.algorithm),\n nistQuantumSecurityLevel: 0,\n cryptoFunctions:\n g.discriminator === \"signature\"\n ? [\"sign\", \"verify\"]\n : g.discriminator === \"kem\"\n ? [\"encapsulate\", \"decapsulate\"]\n : // CycloneDX 1.6 has no \"keyagree\" cryptoFunction; \"other\" is\n // the valid value for a key-agreement primitive.\n [\"other\"],\n },\n };\n label = g.discriminator;\n break;\n }\n\n return {\n type: \"cryptographic-asset\" as const,\n \"bom-ref\": bomRef(key),\n name: `${g.algorithm} (${label})`,\n cryptoProperties: {\n assetType: g.assetType,\n ...typeProps,\n },\n // The quantum posture flags are carried as CycloneDX component `properties`\n // (an open name/value list) rather than inside `cryptoProperties`, whose\n // 1.6 schema is `additionalProperties: false` — so a strict validator\n // accepts the BOM. Namespaced to avoid clashing with other tools' keys.\n properties: [\n {\n name: \"quantakrypto:quantumVulnerable\",\n value: String(isQuantumVulnerable(g.algorithm)),\n },\n { name: \"quantakrypto:harvestNowDecryptLater\", value: String(anyHndl) },\n ],\n evidence: { occurrences },\n };\n });\n\n const serial = `urn:uuid:${stableUuid(result)}`;\n\n return {\n bomFormat: \"CycloneDX\",\n specVersion: \"1.6\",\n serialNumber: serial,\n version: 1,\n metadata: {\n timestamp: result.finishedAt,\n tools: {\n components: [\n {\n type: \"application\",\n name: \"qScan\",\n version: result.toolVersion || VERSION,\n },\n ],\n },\n component: {\n type: \"application\",\n \"bom-ref\": \"root\",\n name: result.root,\n },\n },\n components,\n };\n}\n\n/**\n * Derive a stable UUID-shaped serial from the scan result so re-exporting the\n * same result yields the same serial number (deterministic output). Hashed over the\n * actual finding set (ruleId + location per finding), NOT just the COUNT — two scans\n * with the same number of totally different findings must not collide, since a\n * CycloneDX serialNumber identifies a specific BOM instance. Findings are already in\n * a stable order (scan.ts sorts by file/line/ruleId).\n */\nfunction stableUuid(result: ScanResult): string {\n const hash = createHash(\"sha256\").update(`${result.root}|${result.toolVersion}`, \"utf8\");\n for (const f of result.findings) {\n hash.update(`\\n${f.ruleId}@${f.location.file}:${f.location.line}:${f.location.column ?? 0}`);\n }\n const h = hash.digest(\"hex\");\n // Shape as a v4-ish UUID (variant/version nibbles forced).\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;\n}\n"]}