@exodus/stasis 1.0.0-beta.0 → 1.0.0-beta.10
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 +32 -8
- package/bin/stasis.js +389 -214
- package/package.json +52 -12
- package/src/audit-corrections.js +96 -0
- package/src/audit.js +289 -64
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +14 -22
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +96 -128
- package/src/cmd/bundle.js +1121 -654
- package/src/cmd/diff.js +2 -19
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +7 -19
- package/src/diff.js +80 -129
- package/src/installed-commits.js +182 -0
- package/src/json-stream.js +283 -0
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +49 -116
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +375 -402
- package/src/loaders/rust.js +3254 -242
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -162
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +150 -0
- package/src/metro-transformer.js +3 -2
- package/src/mock.js +57 -165
- package/src/parse.js +4 -16
- package/src/resolve-fields.js +370 -0
- package/src/resolve-node.js +419 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +120 -97
- package/src/scan.js +348 -180
- package/src/vfs-bundle/entries.js +144 -0
- package/src/vfs-bundle/github.js +202 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +647 -0
- package/src/vfs-bundle.js +80 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -44
- package/src/cmd/extract.js +0 -193
package/src/sbom.js
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { compareVersions, valid } from '@preventive/upstream/semver.js'
|
|
2
2
|
import pkg from '../package.json' with { type: 'json' }
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// instances). This module is deliberately free of file/transport concerns: it
|
|
7
|
-
// never reads disk and never touches brotli (`node:zlib`), so it can run
|
|
8
|
-
// wherever an artifact instance can be handed to it. The CLI glue that reads
|
|
9
|
-
// `stasis.code.br`/`stasis.lock.json` off disk (and so pulls in brotli) lives
|
|
10
|
-
// in `src/cmd/sbom.js`.
|
|
4
|
+
// Derive an SPDX 2.3 or CycloneDX 1.5 SBOM from already-parsed stasis artifacts (`Bundle`/`Lockfile`).
|
|
5
|
+
// Deliberately free of file/transport concerns (no disk, no brotli); the disk-reading CLI glue is src/cmd/sbom.js.
|
|
11
6
|
|
|
12
|
-
//
|
|
13
|
-
// name is the published package name, per the command's spec.
|
|
7
|
+
// Tool/creator identity stamped into every generated SBOM.
|
|
14
8
|
const DEFAULT_TOOL = { name: '@exodus/stasis', version: pkg.version, vendor: 'Exodus Movement, Inc.' }
|
|
15
9
|
|
|
16
|
-
//
|
|
17
|
-
// in modern Node, keeping this module dependency-light.
|
|
10
|
+
// Web Crypto (global) instead of node:crypto, keeping this module dependency-light.
|
|
18
11
|
const newUuid = () => globalThis.crypto.randomUUID()
|
|
19
12
|
|
|
20
|
-
// purl
|
|
21
|
-
//
|
|
22
|
-
//
|
|
13
|
+
// purl types we can mint; an ecosystem with no registered type (`soldeer`) gets no purl rather than a fabricated one,
|
|
14
|
+
// and so does a crate vendored from git or from a source not known (`cargo-git`, `cargo-unknown`): `pkg:cargo` names
|
|
15
|
+
// crates.io's crate of that name, which it may not be.
|
|
23
16
|
const PURL_TYPES = new Set(['npm', 'composer', 'cargo', 'github'])
|
|
24
17
|
|
|
25
|
-
// Infer
|
|
26
|
-
// first-party
|
|
27
|
-
// `ecosystem` field
|
|
28
|
-
// field entirely. PHP bundles tag files with the `php` format and map to
|
|
29
|
-
// Composer; everything else (JS/TS, and the Solidity/Rust/Bash source bundles,
|
|
30
|
-
// which all bucket by `package.json`) uses npm-style names.
|
|
18
|
+
// Infer an artifact's ecosystem from its loader formats: PHP bundles map to Composer, everything
|
|
19
|
+
// else to npm. Used for untagged (first-party) buckets and as the fallback for legacy artifacts
|
|
20
|
+
// predating the per-dependency `ecosystem` field.
|
|
31
21
|
function detectEcosystem(artifact) {
|
|
32
22
|
const formats = artifact.formats
|
|
33
23
|
if (formats && typeof formats.values === 'function') {
|
|
@@ -38,19 +28,17 @@ function detectEcosystem(artifact) {
|
|
|
38
28
|
return 'npm'
|
|
39
29
|
}
|
|
40
30
|
|
|
41
|
-
// Legacy scope classification
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// `node_modules`.
|
|
31
|
+
// Legacy scope classification (pre-`ecosystem` artifacts): non-`node_modules` dirs are first-party,
|
|
32
|
+
// except in a Composer bundle where vendor packages live outside `node_modules`. `node_modules` is
|
|
33
|
+
// a path segment, as stasis-core's hasNodeModulesSegment has it: `foo_node_modules/x` is first-party.
|
|
45
34
|
function classifyScope(ecosystem, dir) {
|
|
46
35
|
if (dir === '.') return 'workspace'
|
|
47
36
|
if (ecosystem === 'composer') return 'dependency'
|
|
48
|
-
return dir.includes('node_modules') ? 'dependency' : 'workspace'
|
|
37
|
+
return dir.split('/').includes('node_modules') ? 'dependency' : 'workspace'
|
|
49
38
|
}
|
|
50
39
|
|
|
51
|
-
// Split a package name into
|
|
52
|
-
//
|
|
53
|
-
// the segment before the first slash; cargo crates and bare names have none.
|
|
40
|
+
// Split a package name into purl/CycloneDX namespace (group) + bare name: npm scopes, Composer
|
|
41
|
+
// vendors and GitHub owners use the segment before the first slash; cargo and bare names have none.
|
|
54
42
|
function splitName(ecosystem, name) {
|
|
55
43
|
const namespaced = ecosystem === 'composer' || ecosystem === 'github' ||
|
|
56
44
|
(ecosystem === 'npm' && name.startsWith('@'))
|
|
@@ -61,12 +49,11 @@ function splitName(ecosystem, name) {
|
|
|
61
49
|
return { group: undefined, name }
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
// Build a Package URL (purl)
|
|
65
|
-
//
|
|
66
|
-
// the
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
// names are case-insensitive, so they're lowercased as the spec requires.
|
|
52
|
+
// Build a Package URL (purl) for a known ecosystem, or null when it has no purl type. Each path
|
|
53
|
+
// segment is percent-encoded (npm scope's `@` -> `%40`) while the `/` namespace separator and the
|
|
54
|
+
// `@` before the version stay literal; Composer and GitHub names are lowercased, per the purl spec.
|
|
55
|
+
// The version qualifier is optional in the purl spec, so a versionless workspace package mints a
|
|
56
|
+
// bare-name purl rather than a fabricated version.
|
|
70
57
|
export function buildPurl(ecosystem, rawName, version) {
|
|
71
58
|
if (!PURL_TYPES.has(ecosystem)) return null
|
|
72
59
|
let { group, name } = splitName(ecosystem, rawName)
|
|
@@ -75,93 +62,97 @@ export function buildPurl(ecosystem, rawName, version) {
|
|
|
75
62
|
name = name.toLowerCase()
|
|
76
63
|
}
|
|
77
64
|
const namespace = group ? `${encodeURIComponent(group)}/` : ''
|
|
78
|
-
|
|
65
|
+
const at = version == null ? '' : `@${encodeURIComponent(version)}`
|
|
66
|
+
return `pkg:${ecosystem}/${namespace}${encodeURIComponent(name)}${at}`
|
|
79
67
|
}
|
|
80
68
|
|
|
81
69
|
function compareComponents(a, b) {
|
|
82
70
|
if (a.name !== b.name) return a.name < b.name ? -1 : 1
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
// A missing version sorts first, as '' (comparing raw undefined would be order-unstable).
|
|
72
|
+
const [av, bv] = [a.version ?? '', b.version ?? '']
|
|
73
|
+
if (av !== bv) {
|
|
74
|
+
if (valid(av) && valid(bv)) return compareVersions(av, bv)
|
|
75
|
+
return av < bv ? -1 : 1
|
|
86
76
|
}
|
|
87
77
|
return a.ecosystem < b.ecosystem ? -1 : a.ecosystem > b.ecosystem ? 1 : 0
|
|
88
78
|
}
|
|
89
79
|
|
|
90
|
-
// Collect the deduplicated, sorted set of packages
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// artifact stays workspace regardless of argument order, so the output doesn't
|
|
103
|
-
// depend on how the artifacts are listed.
|
|
80
|
+
// Collect the deduplicated, sorted set of packages across parsed artifacts. Unlike `stasis audit`
|
|
81
|
+
// (installed deps only), an SBOM is the *full* bill of materials, so workspace/first-party packages
|
|
82
|
+
// are included too, tagged scope 'workspace' vs 'dependency'. Buckets without a name are skipped,
|
|
83
|
+
// and so are DEPENDENCY buckets without a version (v0 partial metadata: unverifiable identity) --
|
|
84
|
+
// but a workspace package may legitimately omit version (private/unpublished) and is kept, since
|
|
85
|
+
// dropping it would silently lose the document's own first-party subject. Dedup is by
|
|
86
|
+
// ecosystem+name+version; a package seen as workspace in any artifact stays workspace regardless
|
|
87
|
+
// of argument order, so output doesn't depend on how artifacts are listed.
|
|
88
|
+
// A dependency's `vendored` packages (those copied into it whose files a bundle carries) ride on its
|
|
89
|
+
// component as `vendored`, sorted by `dir`: each its `name`, the `version` its package.json gives if
|
|
90
|
+
// any, and a purl only beside one -- a vendored package.json without a version names no release, and
|
|
91
|
+
// may not name a registry package at all (Next.js calls its React copy `react-builtin`).
|
|
104
92
|
export function collectComponents(artifacts) {
|
|
105
93
|
const seen = new Map()
|
|
106
94
|
for (const artifact of artifacts) {
|
|
107
95
|
const records = [...artifact.modules]
|
|
108
|
-
// The per-dependency `ecosystem` field
|
|
109
|
-
//
|
|
110
|
-
// predates the field and we use the format/path heuristic for everything.
|
|
96
|
+
// The per-dependency `ecosystem` field separates deps (tagged) from first-party buckets (untagged);
|
|
97
|
+
// if no bucket carries it, the artifact predates the field and we use the heuristic for everything.
|
|
111
98
|
const tagged = records.some(([, m]) => m.ecosystem !== undefined)
|
|
112
99
|
const fileEcosystem = detectEcosystem(artifact)
|
|
113
|
-
for (const [dir, { name, version, ecosystem }] of records) {
|
|
114
|
-
if (!name
|
|
100
|
+
for (const [dir, { name, version, ecosystem, vendored }] of records) {
|
|
101
|
+
if (!name) continue
|
|
115
102
|
let scope, eco
|
|
116
103
|
if (ecosystem !== undefined) {
|
|
117
104
|
scope = 'dependency'
|
|
118
105
|
eco = ecosystem
|
|
119
106
|
} else if (tagged) {
|
|
120
|
-
// Untagged bucket in a modern artifact: first-party workspace code.
|
|
121
|
-
// purl ecosystem is inferred (npm, or composer for a PHP project).
|
|
107
|
+
// Untagged bucket in a modern artifact: first-party workspace code; ecosystem inferred.
|
|
122
108
|
scope = 'workspace'
|
|
123
109
|
eco = fileEcosystem
|
|
124
110
|
} else {
|
|
125
111
|
scope = classifyScope(fileEcosystem, dir)
|
|
126
112
|
eco = fileEcosystem
|
|
127
113
|
}
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
if (!version && scope !== 'workspace') continue
|
|
115
|
+
const key = `${eco} ${name} ${version ?? ''}`
|
|
116
|
+
let component = seen.get(key)
|
|
117
|
+
if (component) {
|
|
118
|
+
if (scope === 'workspace') component.scope = 'workspace'
|
|
119
|
+
} else {
|
|
120
|
+
seen.set(key, component = { name, version: version ?? undefined, scope, ecosystem: eco, purl: buildPurl(eco, name, version) })
|
|
121
|
+
}
|
|
122
|
+
// Across artifacts, the union by directory; the first to list one names its package.
|
|
123
|
+
for (const [sub, v] of Object.entries(vendored ?? {})) {
|
|
124
|
+
component.vendored ??= new Map()
|
|
125
|
+
if (component.vendored.has(sub)) continue
|
|
126
|
+
component.vendored.set(sub, { dir: sub, name: v.name, version: v.version, ecosystem: eco, purl: v.version === undefined ? null : buildPurl(eco, v.name, v.version) })
|
|
133
127
|
}
|
|
134
|
-
seen.set(key, { name, version, scope, ecosystem: eco, purl: buildPurl(eco, name, version) })
|
|
135
128
|
}
|
|
136
129
|
}
|
|
130
|
+
for (const component of seen.values()) {
|
|
131
|
+
if (component.vendored) component.vendored = [...component.vendored.values()].toSorted((a, b) => (a.dir < b.dir ? -1 : 1))
|
|
132
|
+
}
|
|
137
133
|
return [...seen.values()].toSorted(compareComponents)
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
// Pick the single component the SBOM is *about* (its primary/root)
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// packages) the BOM has no single subject and every package is listed as a
|
|
144
|
-
// plain component. `rest` is everything that is not the primary.
|
|
136
|
+
// Pick the single component the SBOM is *about* (its primary/root): the lone workspace package when
|
|
137
|
+
// there is exactly one; with zero or several workspace roots there's no single subject, so every
|
|
138
|
+
// package is a plain component. `rest` is everything that is not the primary.
|
|
145
139
|
function selectPrimary(components) {
|
|
146
140
|
const workspace = components.filter((c) => c.scope === 'workspace')
|
|
147
141
|
const primary = workspace.length === 1 ? workspace[0] : null
|
|
148
142
|
return { primary, rest: primary ? components.filter((c) => c !== primary) : components }
|
|
149
143
|
}
|
|
150
144
|
|
|
151
|
-
// SBOM timestamps are RFC 3339
|
|
152
|
-
// `YYYY-MM-DDThh:mm:ssZ`); drop the milliseconds Date.toISOString includes.
|
|
145
|
+
// SBOM timestamps are RFC 3339 to whole seconds; drop the milliseconds Date.toISOString includes.
|
|
153
146
|
const isoSeconds = (date) => date.toISOString().replace(/\.\d{3}Z$/u, 'Z')
|
|
154
147
|
|
|
155
|
-
// A CycloneDX bom-ref must uniquely identify a component
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
const bomRef = (c) => c.purl ?? `${c.ecosystem}:${c.name}@${c.version}`
|
|
148
|
+
// A CycloneDX bom-ref must uniquely identify a component; use the purl when present, else
|
|
149
|
+
// ecosystem+name(+version) (unique by construction -- it's the dedup key).
|
|
150
|
+
const bomRef = (c) => c.purl ?? `${c.ecosystem}:${c.name}${c.version == null ? '' : `@${c.version}`}`
|
|
159
151
|
|
|
160
|
-
// Render an SPDX 2.3 document
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// DESCRIBES the workspace packages, or every package when there are none.
|
|
152
|
+
// Render an SPDX 2.3 document. It DESCRIBES its primary component, which DEPENDS_ON each installed
|
|
153
|
+
// dependency (flat graph). With no single primary, the document DESCRIBES the workspace packages,
|
|
154
|
+
// or every package when there are none. A package vendored into a dependency is a package of its own,
|
|
155
|
+
// after the rest, which that dependency CONTAINS, its directory there its `packageFileName`.
|
|
165
156
|
export function toSpdx(components, { tool = DEFAULT_TOOL, now = new Date(), uuid = newUuid() } = {}) {
|
|
166
157
|
const { primary, rest } = selectPrimary(components)
|
|
167
158
|
const ordered = primary ? [primary, ...rest] : components
|
|
@@ -170,7 +161,8 @@ export function toSpdx(components, { tool = DEFAULT_TOOL, now = new Date(), uuid
|
|
|
170
161
|
const packages = ordered.map((c) => ({
|
|
171
162
|
SPDXID: ids.get(c),
|
|
172
163
|
name: c.name,
|
|
173
|
-
versionInfo
|
|
164
|
+
// SPDX versionInfo is optional; omit it for a versionless workspace package.
|
|
165
|
+
...(c.version == null ? {} : { versionInfo: c.version }),
|
|
174
166
|
downloadLocation: 'NOASSERTION',
|
|
175
167
|
filesAnalyzed: false,
|
|
176
168
|
...(c.purl && {
|
|
@@ -178,6 +170,21 @@ export function toSpdx(components, { tool = DEFAULT_TOOL, now = new Date(), uuid
|
|
|
178
170
|
}),
|
|
179
171
|
primaryPackagePurpose: c.scope === 'workspace' ? 'APPLICATION' : 'LIBRARY',
|
|
180
172
|
}))
|
|
173
|
+
const vendored = ordered.flatMap((c) => (c.vendored ?? []).map((v, i) => ({ host: c, v, id: `${ids.get(c)}-Vendored-${i}` })))
|
|
174
|
+
for (const { v, id } of vendored) {
|
|
175
|
+
packages.push({
|
|
176
|
+
SPDXID: id,
|
|
177
|
+
name: v.name,
|
|
178
|
+
...(v.version == null ? {} : { versionInfo: v.version }),
|
|
179
|
+
packageFileName: v.dir,
|
|
180
|
+
downloadLocation: 'NOASSERTION',
|
|
181
|
+
filesAnalyzed: false,
|
|
182
|
+
...(v.purl && {
|
|
183
|
+
externalRefs: [{ referenceCategory: 'PACKAGE-MANAGER', referenceType: 'purl', referenceLocator: v.purl }],
|
|
184
|
+
}),
|
|
185
|
+
primaryPackagePurpose: 'LIBRARY',
|
|
186
|
+
})
|
|
187
|
+
}
|
|
181
188
|
|
|
182
189
|
const relationships = []
|
|
183
190
|
if (primary) {
|
|
@@ -193,8 +200,13 @@ export function toSpdx(components, { tool = DEFAULT_TOOL, now = new Date(), uuid
|
|
|
193
200
|
relationships.push({ spdxElementId: 'SPDXRef-DOCUMENT', relatedSpdxElement: ids.get(c), relationshipType: 'DESCRIBES' })
|
|
194
201
|
}
|
|
195
202
|
}
|
|
203
|
+
for (const { host, id } of vendored) {
|
|
204
|
+
relationships.push({ spdxElementId: ids.get(host), relatedSpdxElement: id, relationshipType: 'CONTAINS' })
|
|
205
|
+
}
|
|
196
206
|
|
|
197
|
-
const docName = primary
|
|
207
|
+
const docName = primary
|
|
208
|
+
? (primary.version == null ? primary.name : `${primary.name}@${primary.version}`)
|
|
209
|
+
: 'stasis-sbom'
|
|
198
210
|
return {
|
|
199
211
|
spdxVersion: 'SPDX-2.3',
|
|
200
212
|
dataLicense: 'CC0-1.0',
|
|
@@ -210,10 +222,10 @@ export function toSpdx(components, { tool = DEFAULT_TOOL, now = new Date(), uuid
|
|
|
210
222
|
}
|
|
211
223
|
}
|
|
212
224
|
|
|
213
|
-
// Render a CycloneDX 1.5 document
|
|
214
|
-
// components
|
|
215
|
-
//
|
|
216
|
-
//
|
|
225
|
+
// Render a CycloneDX 1.5 document. The primary (workspace root) becomes metadata.component; the
|
|
226
|
+
// rest go in `components`, with a flat `dependencies` edge from primary to each installed dependency.
|
|
227
|
+
// A package vendored into a dependency is nested in that dependency's `components`, its directory
|
|
228
|
+
// there the location of its one occurrence.
|
|
217
229
|
export function toCyclonedx(components, { tool = DEFAULT_TOOL, now = new Date(), uuid = newUuid() } = {}) {
|
|
218
230
|
const { primary, rest } = selectPrimary(components)
|
|
219
231
|
const toComponent = (c) => {
|
|
@@ -223,8 +235,24 @@ export function toCyclonedx(components, { tool = DEFAULT_TOOL, now = new Date(),
|
|
|
223
235
|
'bom-ref': bomRef(c),
|
|
224
236
|
...(group && { group }),
|
|
225
237
|
name,
|
|
226
|
-
version
|
|
238
|
+
// CycloneDX 1.5 `version` is optional; omit it for a versionless workspace package.
|
|
239
|
+
...(c.version == null ? {} : { version: c.version }),
|
|
227
240
|
...(c.purl && { purl: c.purl }),
|
|
241
|
+
...(c.vendored && { components: c.vendored.map((v) => toVendoredComponent(c, v)) }),
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Its bom-ref the host's with the directory as a purl subpath: two copies of one package (Next.js
|
|
245
|
+
// has loader-utils2 and loader-utils3, both `loader-utils`) stay apart.
|
|
246
|
+
const toVendoredComponent = (host, v) => {
|
|
247
|
+
const { group, name } = splitName(v.ecosystem, v.name)
|
|
248
|
+
return {
|
|
249
|
+
type: 'library',
|
|
250
|
+
'bom-ref': `${bomRef(host)}#${v.dir}`,
|
|
251
|
+
...(group && { group }),
|
|
252
|
+
name,
|
|
253
|
+
...(v.version == null ? {} : { version: v.version }),
|
|
254
|
+
...(v.purl && { purl: v.purl }),
|
|
255
|
+
evidence: { occurrences: [{ location: v.dir }] },
|
|
228
256
|
}
|
|
229
257
|
}
|
|
230
258
|
|
|
@@ -235,8 +263,7 @@ export function toCyclonedx(components, { tool = DEFAULT_TOOL, now = new Date(),
|
|
|
235
263
|
version: 1,
|
|
236
264
|
metadata: {
|
|
237
265
|
timestamp: isoSeconds(now),
|
|
238
|
-
// CycloneDX 1.5 `tools.components` shape: the flat tools array is
|
|
239
|
-
// deprecated in 1.5 (removed in 1.6), and newer consumers read this form.
|
|
266
|
+
// CycloneDX 1.5 `tools.components` shape: the flat tools array is deprecated in 1.5 (removed in 1.6).
|
|
240
267
|
tools: { components: [{ type: 'application', publisher: tool.vendor, name: tool.name, version: tool.version }] },
|
|
241
268
|
...(primary && { component: toComponent(primary) }),
|
|
242
269
|
},
|
|
@@ -249,18 +276,14 @@ export function toCyclonedx(components, { tool = DEFAULT_TOOL, now = new Date(),
|
|
|
249
276
|
return doc
|
|
250
277
|
}
|
|
251
278
|
|
|
252
|
-
// Dispatch to the requested format
|
|
253
|
-
// `collectComponents`; `options` accepts `{ tool, now, uuid }` for custom
|
|
254
|
-
// tool identity / deterministic output.
|
|
279
|
+
// Dispatch to the requested format; options: { tool, now, uuid } for custom identity / deterministic output.
|
|
255
280
|
export function generateSbom(format, components, options) {
|
|
256
281
|
if (format === 'spdx') return toSpdx(components, options)
|
|
257
282
|
if (format === 'cyclonedx') return toCyclonedx(components, options)
|
|
258
283
|
throw new Error(`sbom: format must be spdx or cyclonedx, got ${format}`)
|
|
259
284
|
}
|
|
260
285
|
|
|
261
|
-
// Convenience one-shot:
|
|
262
|
-
// render them in `format`. Equivalent to
|
|
263
|
-
// `generateSbom(format, collectComponents(artifacts), options)`.
|
|
286
|
+
// Convenience one-shot: collectComponents then generateSbom.
|
|
264
287
|
export function sbom(format, artifacts, options) {
|
|
265
288
|
return generateSbom(format, collectComponents(artifacts), options)
|
|
266
289
|
}
|