@quantakrypto/core 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -27
- package/dist/cbom-merge.d.ts.map +1 -1
- package/dist/cbom-merge.js +30 -8
- package/dist/cbom-merge.js.map +1 -1
- package/dist/cbom.d.ts +5 -0
- package/dist/cbom.d.ts.map +1 -1
- package/dist/cbom.js +132 -37
- package/dist/cbom.js.map +1 -1
- package/dist/comments.d.ts +2 -23
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +24 -5
- package/dist/comments.js.map +1 -1
- package/dist/detect-utils.d.ts +55 -21
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +133 -7
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/age.d.ts +16 -0
- package/dist/detectors/age.d.ts.map +1 -0
- package/dist/detectors/age.js +40 -0
- package/dist/detectors/age.js.map +1 -0
- package/dist/detectors/ansible.d.ts +13 -0
- package/dist/detectors/ansible.d.ts.map +1 -0
- package/dist/detectors/ansible.js +108 -0
- package/dist/detectors/ansible.js.map +1 -0
- package/dist/detectors/bicep.d.ts +20 -0
- package/dist/detectors/bicep.d.ts.map +1 -0
- package/dist/detectors/bicep.js +74 -0
- package/dist/detectors/bicep.js.map +1 -0
- package/dist/detectors/c.d.ts.map +1 -1
- package/dist/detectors/c.js +30 -3
- package/dist/detectors/c.js.map +1 -1
- package/dist/detectors/cicd.d.ts.map +1 -1
- package/dist/detectors/cicd.js +18 -5
- package/dist/detectors/cicd.js.map +1 -1
- package/dist/detectors/cloud-kms.d.ts +8 -5
- package/dist/detectors/cloud-kms.d.ts.map +1 -1
- package/dist/detectors/cloud-kms.js +62 -20
- package/dist/detectors/cloud-kms.js.map +1 -1
- package/dist/detectors/cloudformation.d.ts +8 -0
- package/dist/detectors/cloudformation.d.ts.map +1 -1
- package/dist/detectors/cloudformation.js +41 -10
- package/dist/detectors/cloudformation.js.map +1 -1
- package/dist/detectors/codesign.d.ts +50 -0
- package/dist/detectors/codesign.d.ts.map +1 -0
- package/dist/detectors/codesign.js +143 -0
- package/dist/detectors/codesign.js.map +1 -0
- package/dist/detectors/csharp.d.ts.map +1 -1
- package/dist/detectors/csharp.js +13 -3
- package/dist/detectors/csharp.js.map +1 -1
- package/dist/detectors/dart.d.ts +35 -0
- package/dist/detectors/dart.d.ts.map +1 -0
- package/dist/detectors/dart.js +177 -0
- package/dist/detectors/dart.js.map +1 -0
- package/dist/detectors/database.d.ts +5 -3
- package/dist/detectors/database.d.ts.map +1 -1
- package/dist/detectors/database.js +45 -8
- package/dist/detectors/database.js.map +1 -1
- package/dist/detectors/dkim.d.ts +54 -0
- package/dist/detectors/dkim.d.ts.map +1 -0
- package/dist/detectors/dkim.js +92 -0
- package/dist/detectors/dkim.js.map +1 -0
- package/dist/detectors/dnssec.d.ts.map +1 -1
- package/dist/detectors/dnssec.js +19 -10
- package/dist/detectors/dnssec.js.map +1 -1
- package/dist/detectors/elixir.d.ts.map +1 -1
- package/dist/detectors/elixir.js +34 -2
- package/dist/detectors/elixir.js.map +1 -1
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +33 -3
- package/dist/detectors/java.js.map +1 -1
- package/dist/detectors/jose.d.ts.map +1 -1
- package/dist/detectors/jose.js +13 -3
- package/dist/detectors/jose.js.map +1 -1
- package/dist/detectors/jwk.d.ts +7 -7
- package/dist/detectors/jwk.d.ts.map +1 -1
- package/dist/detectors/jwk.js +44 -8
- package/dist/detectors/jwk.js.map +1 -1
- package/dist/detectors/k8s.d.ts.map +1 -1
- package/dist/detectors/k8s.js +5 -2
- package/dist/detectors/k8s.js.map +1 -1
- package/dist/detectors/keystore.d.ts +19 -0
- package/dist/detectors/keystore.d.ts.map +1 -0
- package/dist/detectors/keystore.js +76 -0
- package/dist/detectors/keystore.js.map +1 -0
- package/dist/detectors/mesh.d.ts +7 -8
- package/dist/detectors/mesh.d.ts.map +1 -1
- package/dist/detectors/mesh.js +17 -35
- package/dist/detectors/mesh.js.map +1 -1
- package/dist/detectors/messaging.d.ts +2 -2
- package/dist/detectors/messaging.d.ts.map +1 -1
- package/dist/detectors/messaging.js +19 -12
- package/dist/detectors/messaging.js.map +1 -1
- package/dist/detectors/objc.d.ts +41 -0
- package/dist/detectors/objc.d.ts.map +1 -0
- package/dist/detectors/objc.js +146 -0
- package/dist/detectors/objc.js.map +1 -0
- package/dist/detectors/openpgp.d.ts +20 -0
- package/dist/detectors/openpgp.d.ts.map +1 -0
- package/dist/detectors/openpgp.js +151 -0
- package/dist/detectors/openpgp.js.map +1 -0
- package/dist/detectors/pem.d.ts.map +1 -1
- package/dist/detectors/pem.js +32 -0
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/php.d.ts +2 -1
- package/dist/detectors/php.d.ts.map +1 -1
- package/dist/detectors/php.js +7 -3
- package/dist/detectors/php.js.map +1 -1
- package/dist/detectors/pkcs11.d.ts +17 -0
- package/dist/detectors/pkcs11.d.ts.map +1 -0
- package/dist/detectors/pkcs11.js +87 -0
- package/dist/detectors/pkcs11.js.map +1 -0
- package/dist/detectors/proxy.d.ts +52 -0
- package/dist/detectors/proxy.d.ts.map +1 -0
- package/dist/detectors/proxy.js +157 -0
- package/dist/detectors/proxy.js.map +1 -0
- package/dist/detectors/pulumi.d.ts +19 -0
- package/dist/detectors/pulumi.d.ts.map +1 -0
- package/dist/detectors/pulumi.js +80 -0
- package/dist/detectors/pulumi.js.map +1 -0
- package/dist/detectors/rust.d.ts.map +1 -1
- package/dist/detectors/rust.js +5 -0
- package/dist/detectors/rust.js.map +1 -1
- package/dist/detectors/secrets.d.ts +4 -1
- package/dist/detectors/secrets.d.ts.map +1 -1
- package/dist/detectors/secrets.js +5 -21
- package/dist/detectors/secrets.js.map +1 -1
- package/dist/detectors/solidity.d.ts +50 -0
- package/dist/detectors/solidity.d.ts.map +1 -0
- package/dist/detectors/solidity.js +108 -0
- package/dist/detectors/solidity.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +53 -6
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/spire.d.ts +49 -0
- package/dist/detectors/spire.d.ts.map +1 -0
- package/dist/detectors/spire.js +77 -0
- package/dist/detectors/spire.js.map +1 -0
- package/dist/detectors/ssh-ca.d.ts +72 -0
- package/dist/detectors/ssh-ca.d.ts.map +1 -0
- package/dist/detectors/ssh-ca.js +119 -0
- package/dist/detectors/ssh-ca.js.map +1 -0
- package/dist/detectors/supply-chain.d.ts +17 -0
- package/dist/detectors/supply-chain.d.ts.map +1 -0
- package/dist/detectors/supply-chain.js +89 -0
- package/dist/detectors/supply-chain.js.map +1 -0
- package/dist/detectors/swift.d.ts +22 -0
- package/dist/detectors/swift.d.ts.map +1 -0
- package/dist/detectors/swift.js +120 -0
- package/dist/detectors/swift.js.map +1 -0
- package/dist/detectors/terraform.d.ts.map +1 -1
- package/dist/detectors/terraform.js +35 -8
- package/dist/detectors/terraform.js.map +1 -1
- package/dist/detectors/vault.d.ts +15 -0
- package/dist/detectors/vault.d.ts.map +1 -0
- package/dist/detectors/vault.js +97 -0
- package/dist/detectors/vault.js.map +1 -0
- package/dist/detectors/vpn.d.ts +8 -8
- package/dist/detectors/vpn.d.ts.map +1 -1
- package/dist/detectors/vpn.js +12 -38
- package/dist/detectors/vpn.js.map +1 -1
- package/dist/detectors/weak-hash.d.ts +71 -0
- package/dist/detectors/weak-hash.d.ts.map +1 -0
- package/dist/detectors/weak-hash.js +116 -0
- package/dist/detectors/weak-hash.js.map +1 -0
- package/dist/detectors/webauthn.d.ts +61 -0
- package/dist/detectors/webauthn.d.ts.map +1 -0
- package/dist/detectors/webauthn.js +109 -0
- package/dist/detectors/webauthn.js.map +1 -0
- package/dist/detectors/xmldsig.d.ts +23 -0
- package/dist/detectors/xmldsig.d.ts.map +1 -0
- package/dist/detectors/xmldsig.js +84 -0
- package/dist/detectors/xmldsig.js.map +1 -0
- package/dist/evidence.d.ts +74 -4
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +71 -0
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +14 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -11
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +0 -2
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +15 -6
- package/dist/inventory.js.map +1 -1
- package/dist/registry.d.ts +9 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +51 -4
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +0 -3
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +1 -1
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/remediation.d.ts +10 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +32 -0
- package/dist/remediation.js.map +1 -1
- package/dist/report.d.ts +9 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +31 -1
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js +5 -3
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +35 -6
- package/dist/scan.js.map +1 -1
- package/dist/standards-profiles.d.ts +58 -0
- package/dist/standards-profiles.d.ts.map +1 -0
- package/dist/standards-profiles.js +100 -0
- package/dist/standards-profiles.js.map +1 -0
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/vex.d.ts +39 -0
- package/dist/vex.d.ts.map +1 -0
- package/dist/vex.js +87 -0
- package/dist/vex.js.map +1 -0
- package/dist/walk.d.ts +8 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +32 -1
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +38 -8
- package/src/cbom.ts +153 -39
- package/src/comments.ts +25 -6
- package/src/detect-utils.ts +133 -11
- package/src/detectors/age.ts +60 -0
- package/src/detectors/ansible.ts +140 -0
- package/src/detectors/bicep.ts +107 -0
- package/src/detectors/c.ts +31 -3
- package/src/detectors/cicd.ts +18 -5
- package/src/detectors/cloud-kms.ts +79 -25
- package/src/detectors/cloudformation.ts +45 -10
- package/src/detectors/codesign.ts +214 -0
- package/src/detectors/csharp.ts +13 -3
- package/src/detectors/dart.ts +237 -0
- package/src/detectors/database.ts +64 -11
- package/src/detectors/dkim.ts +172 -0
- package/src/detectors/dnssec.ts +19 -10
- package/src/detectors/elixir.ts +33 -2
- package/src/detectors/java.ts +37 -3
- package/src/detectors/jose.ts +19 -3
- package/src/detectors/jwk.ts +59 -17
- package/src/detectors/k8s.ts +5 -2
- package/src/detectors/keystore.ts +109 -0
- package/src/detectors/mesh.ts +21 -45
- package/src/detectors/messaging.ts +22 -14
- package/src/detectors/objc.ts +209 -0
- package/src/detectors/openpgp.ts +194 -0
- package/src/detectors/pem.ts +30 -0
- package/src/detectors/php.ts +9 -4
- package/src/detectors/pkcs11.ts +113 -0
- package/src/detectors/proxy.ts +236 -0
- package/src/detectors/pulumi.ts +106 -0
- package/src/detectors/rust.ts +5 -0
- package/src/detectors/secrets.ts +15 -26
- package/src/detectors/solidity.ts +176 -0
- package/src/detectors/source.ts +48 -5
- package/src/detectors/spire.ts +143 -0
- package/src/detectors/ssh-ca.ts +215 -0
- package/src/detectors/supply-chain.ts +117 -0
- package/src/detectors/swift.ts +149 -0
- package/src/detectors/terraform.ts +42 -8
- package/src/detectors/vault.ts +120 -0
- package/src/detectors/vpn.ts +20 -48
- package/src/detectors/weak-hash.ts +217 -0
- package/src/detectors/webauthn.ts +205 -0
- package/src/detectors/xmldsig.ts +115 -0
- package/src/evidence.ts +127 -4
- package/src/index.ts +41 -13
- package/src/inventory.ts +15 -7
- package/src/registry.ts +51 -4
- package/src/remediate-pipeline.ts +1 -1
- package/src/remediation.ts +39 -0
- package/src/report.ts +36 -1
- package/src/scan-worker.ts +5 -3
- package/src/scan.ts +41 -6
- package/src/standards-profiles.ts +136 -0
- package/src/types.ts +9 -4
- package/src/version.ts +1 -1
- package/src/vex.ts +122 -0
- package/src/walk.ts +33 -2
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
|
|
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.
|
|
143
|
-
detection to other ecosystems
|
|
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
|
-
|
|
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` (
|
|
172
|
-
`scope` (`"source" | "config"`), an `appliesTo(path)` extension
|
|
173
|
-
`detect({ file, content })` returning `Finding[]
|
|
174
|
-
`
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 (**
|
|
186
|
-
asymmetric crypto, spanning **
|
|
187
|
-
Maven, and
|
|
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`
|
|
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,
|
|
254
|
-
occurrence evidence and `quantumVulnerable` / `harvestNowDecryptLater` flags.
|
|
255
|
-
|
|
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
|
-
| `
|
|
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
|
|
327
|
+
| `DetectorRegistry`, `defaultRegistry` | class/const | Detector plugin point |
|
|
281
328
|
| `buildInventory` | fn | Aggregate findings → `CryptoInventory` |
|
|
282
|
-
| `vulnerableDependencies` | const | Curated quantum-vulnerable
|
|
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 |
|
package/dist/cbom-merge.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/cbom-merge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/cbom-merge.js.map
CHANGED
|
@@ -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
|
/**
|
package/dist/cbom.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cbom.d.ts","sourceRoot":"","sources":["../src/cbom.ts"],"names":[],"mappings":"
|
|
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/).
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
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 "
|
|
24
|
-
|
|
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 |
|
|
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
|
|
84
|
-
const key = `${algorithm}|${
|
|
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 = {
|
|
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} (${
|
|
198
|
+
name: `${g.algorithm} (${label})`,
|
|
106
199
|
cryptoProperties: {
|
|
107
|
-
assetType:
|
|
108
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
.
|
|
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":"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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/). 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"]}
|