mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-18 22:02:53 -04:00
feat(sbom): added support for specifying --sbom-spec-version (#11389)
Currently only supported for cyclonedx. Allowed values are 1.5, 1.6, and 1.7 (defaulting to 1.7); the lower bound is 1.5 because the generated JSON uses fields (e.g. `metadata.lifecycles`) that are only valid from CycloneDX 1.5+. --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
committed by
GitHub
parent
6d7903a8b7
commit
87b4bac2bd
6
.changeset/honest-moose-grin.md
Normal file
6
.changeset/honest-moose-grin.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/deps.compliance.sbom": minor
|
||||
pnpm: minor
|
||||
---
|
||||
|
||||
Allow setting sbom spec version using `--sbom-spec-version` [#11389](https://github.com/pnpm/pnpm/pull/11389).
|
||||
30
deps/compliance/commands/src/sbom/sbom.ts
vendored
30
deps/compliance/commands/src/sbom/sbom.ts
vendored
@@ -20,6 +20,7 @@ import { renderHelp } from 'render-help'
|
||||
export type SbomCommandOptions = {
|
||||
sbomFormat?: string
|
||||
sbomType?: string
|
||||
sbomSpecVersion?: string
|
||||
lockfileOnly?: boolean
|
||||
sbomAuthors?: string
|
||||
sbomSupplier?: string
|
||||
@@ -55,6 +56,7 @@ export const cliOptionsTypes = (): Record<string, unknown> => ({
|
||||
recursive: Boolean,
|
||||
'sbom-format': String,
|
||||
'sbom-type': String,
|
||||
'sbom-spec-version': String,
|
||||
'sbom-authors': String,
|
||||
'sbom-supplier': String,
|
||||
'lockfile-only': Boolean,
|
||||
@@ -82,6 +84,10 @@ export function help (): string {
|
||||
description: 'The component type for the root package (default: library)',
|
||||
name: '--sbom-type <library|application>',
|
||||
},
|
||||
{
|
||||
description: 'The CycloneDX specification version (1.5, 1.6, or 1.7; default: 1.7)',
|
||||
name: '--sbom-spec-version <version>',
|
||||
},
|
||||
{
|
||||
description: 'Only use lockfile data (skip reading from the store)',
|
||||
name: '--lockfile-only',
|
||||
@@ -143,6 +149,7 @@ export async function handler (
|
||||
}
|
||||
|
||||
const sbomType = validateSbomType(opts.sbomType)
|
||||
const sbomSpecVersion = validateSbomSpecVersion(opts.sbomSpecVersion, format)
|
||||
|
||||
const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, {
|
||||
ignoreIncompatible: true,
|
||||
@@ -217,6 +224,7 @@ export async function handler (
|
||||
lockfileOnly: opts.lockfileOnly,
|
||||
sbomAuthors: opts.sbomAuthors?.split(',').map((s) => s.trim()).filter(Boolean),
|
||||
sbomSupplier: opts.sbomSupplier,
|
||||
specVersion: sbomSpecVersion,
|
||||
})
|
||||
: serializeSpdx(result)
|
||||
|
||||
@@ -231,3 +239,25 @@ function validateSbomType (value: string | undefined): SbomComponentType {
|
||||
`Invalid SBOM type "${value}". Use "library" or "application".`
|
||||
)
|
||||
}
|
||||
|
||||
// Versions whose schema is fully covered by what we currently emit
|
||||
// (e.g. metadata.lifecycles requires CycloneDX 1.5+).
|
||||
const SUPPORTED_CYCLONEDX_SPEC_VERSIONS = ['1.5', '1.6', '1.7']
|
||||
|
||||
function validateSbomSpecVersion (value: string | undefined, format: SbomFormat): string | undefined {
|
||||
if (value == null) return undefined
|
||||
if (format !== 'cyclonedx') {
|
||||
throw new PnpmError(
|
||||
'SBOM_SPEC_VERSION_UNSUPPORTED_FORMAT',
|
||||
'The --sbom-spec-version option is only supported with --sbom-format cyclonedx.'
|
||||
)
|
||||
}
|
||||
const normalized = value.trim()
|
||||
if (!SUPPORTED_CYCLONEDX_SPEC_VERSIONS.includes(normalized)) {
|
||||
throw new PnpmError(
|
||||
'SBOM_INVALID_SPEC_VERSION',
|
||||
`Invalid CycloneDX spec version "${value}". Supported versions: ${SUPPORTED_CYCLONEDX_SPEC_VERSIONS.join(', ')}.`
|
||||
)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
55
deps/compliance/commands/test/sbom/index.ts
vendored
55
deps/compliance/commands/test/sbom/index.ts
vendored
@@ -203,6 +203,61 @@ test('pnpm sbom invalid --sbom-type throws', async () => {
|
||||
).rejects.toThrow('Invalid SBOM type')
|
||||
})
|
||||
|
||||
test('pnpm sbom --sbom-spec-version 1.6', async () => {
|
||||
const workspaceDir = tempDir()
|
||||
f.copy('simple-sbom', workspaceDir)
|
||||
|
||||
const { output, exitCode } = await sbom.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: workspaceDir,
|
||||
lockfileDir: workspaceDir,
|
||||
pnpmHomeDir: '',
|
||||
sbomFormat: 'cyclonedx',
|
||||
sbomSpecVersion: '1.6',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const parsed = JSON.parse(output)
|
||||
expect(parsed.specVersion).toBe('1.6')
|
||||
expect(parsed.$schema).toBe('http://cyclonedx.org/schema/bom-1.6.schema.json')
|
||||
})
|
||||
|
||||
test('pnpm sbom invalid --sbom-spec-version throws', async () => {
|
||||
const workspaceDir = tempDir()
|
||||
f.copy('simple-sbom', workspaceDir)
|
||||
|
||||
await expect(
|
||||
sbom.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: workspaceDir,
|
||||
lockfileDir: workspaceDir,
|
||||
pnpmHomeDir: '',
|
||||
sbomFormat: 'cyclonedx',
|
||||
sbomSpecVersion: '1.4',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
).rejects.toThrow('Invalid CycloneDX spec version')
|
||||
})
|
||||
|
||||
test('pnpm sbom --sbom-spec-version with spdx format throws', async () => {
|
||||
const workspaceDir = tempDir()
|
||||
f.copy('simple-sbom', workspaceDir)
|
||||
|
||||
await expect(
|
||||
sbom.handler({
|
||||
...DEFAULT_OPTS,
|
||||
dir: workspaceDir,
|
||||
lockfileDir: workspaceDir,
|
||||
pnpmHomeDir: '',
|
||||
sbomFormat: 'spdx',
|
||||
sbomSpecVersion: '1.6',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
).rejects.toThrow('only supported with --sbom-format cyclonedx')
|
||||
})
|
||||
|
||||
test('pnpm sbom --sbom-type application', async () => {
|
||||
const workspaceDir = tempDir()
|
||||
f.copy('simple-sbom', workspaceDir)
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface CycloneDxOptions {
|
||||
lockfileOnly?: boolean
|
||||
sbomAuthors?: string[]
|
||||
sbomSupplier?: string
|
||||
specVersion?: string
|
||||
}
|
||||
|
||||
export function serializeCycloneDx (result: SbomResult, opts?: CycloneDxOptions): string {
|
||||
@@ -155,10 +156,12 @@ export function serializeCycloneDx (result: SbomResult, opts?: CycloneDxOptions)
|
||||
metadata.supplier = { name: opts.sbomSupplier }
|
||||
}
|
||||
|
||||
const version = opts?.specVersion || '1.7'
|
||||
|
||||
const bom: Record<string, unknown> = {
|
||||
$schema: 'http://cyclonedx.org/schema/bom-1.7.schema.json',
|
||||
$schema: `http://cyclonedx.org/schema/bom-${version}.schema.json`,
|
||||
bomFormat: 'CycloneDX',
|
||||
specVersion: '1.7',
|
||||
specVersion: version,
|
||||
serialNumber: `urn:uuid:${crypto.randomUUID()}`,
|
||||
version: 1,
|
||||
metadata,
|
||||
|
||||
@@ -57,6 +57,14 @@ describe('serializeCycloneDx', () => {
|
||||
expect(parsed.serialNumber).toMatch(/^urn:uuid:[0-9a-f-]+$/)
|
||||
})
|
||||
|
||||
it('should honor specVersion option in $schema and specVersion fields', () => {
|
||||
const result = makeSbomResult()
|
||||
const parsed = JSON.parse(serializeCycloneDx(result, { specVersion: '1.6' }))
|
||||
|
||||
expect(parsed.$schema).toBe('http://cyclonedx.org/schema/bom-1.6.schema.json')
|
||||
expect(parsed.specVersion).toBe('1.6')
|
||||
})
|
||||
|
||||
it('should include timestamp in metadata', () => {
|
||||
const before = new Date().toISOString()
|
||||
const result = makeSbomResult()
|
||||
|
||||
Reference in New Issue
Block a user