PR-URL: https://github.com/nodejs/node/pull/56818 Reviewed-By: Jordan Harband <ljharb@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
595 lines
14 KiB
JavaScript
595 lines
14 KiB
JavaScript
const t = require('tap')
|
|
const mockNpm = require('../../fixtures/mock-npm.js')
|
|
|
|
const FAKE_TIMESTAMP = '2020-01-01T00:00:00.000Z'
|
|
const FAKE_UUID = '00000000-0000-0000-0000-000000000000'
|
|
|
|
t.cleanSnapshot = s => {
|
|
let sbom
|
|
|
|
try {
|
|
sbom = JSON.parse(s)
|
|
} catch (e) {
|
|
return s
|
|
}
|
|
|
|
// Clean dynamic values from snapshots. SPDX and CycloneDX have different
|
|
// formats for these values, so we need to do it separately.
|
|
if (sbom.SPDXID) {
|
|
sbom.documentNamespace = `http://spdx.org/spdxdocs/test-npm-sbom-1.0.0-${FAKE_UUID}`
|
|
|
|
if (sbom.creationInfo) {
|
|
sbom.creationInfo.created = FAKE_TIMESTAMP
|
|
sbom.creationInfo.creators = ['Tool: npm/cli-10.0.0']
|
|
}
|
|
} else {
|
|
sbom.serialNumber = `urn:uuid:${FAKE_UUID}`
|
|
|
|
if (sbom.metadata) {
|
|
sbom.metadata.timestamp = FAKE_TIMESTAMP
|
|
sbom.metadata.tools[0].version = '10.0.0'
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(sbom, null, 2)
|
|
}
|
|
|
|
const simpleNmFixture = {
|
|
node_modules: {
|
|
foo: {
|
|
'package.json': JSON.stringify({
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
dog: '^1.0.0',
|
|
},
|
|
}),
|
|
node_modules: {
|
|
dog: {
|
|
'package.json': JSON.stringify({
|
|
name: 'dog',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
chai: {
|
|
'package.json': JSON.stringify({
|
|
name: 'chai',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
const mockSbom = async (t, { mocks, config, ...opts } = {}) => {
|
|
const mock = await mockNpm(t, {
|
|
...opts,
|
|
config: {
|
|
...config,
|
|
},
|
|
command: 'sbom',
|
|
mocks: {
|
|
'node:path': {
|
|
...require('node:path'),
|
|
sep: '/',
|
|
},
|
|
...mocks,
|
|
},
|
|
})
|
|
|
|
return {
|
|
...mock,
|
|
result: () => mock.joinedOutput(),
|
|
}
|
|
}
|
|
|
|
t.test('sbom', async t => {
|
|
t.test('basic sbom - spdx', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('basic sbom - cyclonedx', async t => {
|
|
const config = {
|
|
'sbom-format': 'cyclonedx',
|
|
'sbom-type': 'application',
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('--omit dev', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
omit: ['dev'],
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
},
|
|
devDependencies: {
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('--omit optional', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
omit: ['optional'],
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
chai: '^1.0.0',
|
|
},
|
|
optionalDependencies: {
|
|
foo: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('--omit peer', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
omit: ['peer'],
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
chai: '^1.0.0',
|
|
},
|
|
peerDependencies: {
|
|
foo: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
const dupDepsNmFixture = {
|
|
node_modules: {
|
|
foo: {
|
|
'package.json': JSON.stringify({
|
|
name: 'foo',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
node_modules: {
|
|
chai: {
|
|
'package.json': JSON.stringify({
|
|
name: 'chai',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
bar: {
|
|
'package.json': JSON.stringify({
|
|
name: 'bar',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
node_modules: {
|
|
chai: {
|
|
'package.json': JSON.stringify({
|
|
name: 'chai',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
chai: {
|
|
'package.json': JSON.stringify({
|
|
name: 'chai',
|
|
version: '2.0.0',
|
|
}),
|
|
},
|
|
},
|
|
}
|
|
|
|
t.test('duplicate deps - spdx', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
bar: '^1.0.0',
|
|
chai: '^2.0.0',
|
|
},
|
|
}),
|
|
...dupDepsNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('duplicate deps - cyclonedx', async t => {
|
|
const config = {
|
|
'sbom-format': 'cyclonedx',
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
bar: '^1.0.0',
|
|
chai: '^2.0.0',
|
|
},
|
|
}),
|
|
...dupDepsNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('missing format', async t => {
|
|
const config = {}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-sbom',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await t.rejects(sbom.exec([]), {
|
|
code: 'EUSAGE',
|
|
message: 'Must specify --sbom-format flag with one of: cyclonedx, spdx.',
|
|
},
|
|
'should throw error')
|
|
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('invalid dep', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
}
|
|
const { sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-ls',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^2.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await t.rejects(
|
|
sbom.exec([]),
|
|
{ code: 'ESBOMPROBLEMS', message: /invalid: foo@1.0.0/ },
|
|
'should list dep problems'
|
|
)
|
|
})
|
|
|
|
t.test('missing dep', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
}
|
|
const { sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-ls',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
ipsum: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await t.rejects(
|
|
sbom.exec([]),
|
|
{ code: 'ESBOMPROBLEMS', message: /missing: ipsum@\^1.0.0/ },
|
|
'should list dep problems'
|
|
)
|
|
})
|
|
|
|
t.test('missing (optional) dep', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-ls',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
chai: '^1.0.0',
|
|
},
|
|
optionalDependencies: {
|
|
ipsum: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('extraneous dep', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-ls',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
},
|
|
}),
|
|
...simpleNmFixture,
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('lock file only', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
'package-lock-only': true,
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-ls',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
'package-lock.json': JSON.stringify({
|
|
dependencies: {
|
|
foo: {
|
|
version: '1.0.0',
|
|
requires: {
|
|
dog: '^1.0.0',
|
|
},
|
|
},
|
|
dog: {
|
|
version: '1.0.0',
|
|
},
|
|
chai: {
|
|
version: '1.0.0',
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await sbom.exec([])
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('lock file only - missing lock file', async t => {
|
|
const config = {
|
|
'sbom-format': 'spdx',
|
|
'package-lock-only': true,
|
|
}
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'test-npm-ls',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.0.0',
|
|
chai: '^1.0.0',
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
await t.rejects(sbom.exec([]), {
|
|
code: 'EUSAGE',
|
|
message: 'A package lock or shrinkwrap file is required in package-lock-only mode',
|
|
},
|
|
'should throw error')
|
|
|
|
t.matchSnapshot(result())
|
|
})
|
|
|
|
t.test('loading a tree containing workspaces', async t => {
|
|
const mockWorkspaces = async (t, exec = [], config = {}) => {
|
|
const { result, sbom } = await mockSbom(t, {
|
|
config,
|
|
prefixDir: {
|
|
'package.json': JSON.stringify({
|
|
name: 'workspaces-tree',
|
|
version: '1.0.0',
|
|
workspaces: ['./a', './b', './d', './group/*'],
|
|
dependencies: { pacote: '1.0.0' },
|
|
}),
|
|
node_modules: {
|
|
a: t.fixture('symlink', '../a'),
|
|
b: t.fixture('symlink', '../b'),
|
|
c: {
|
|
'package.json': JSON.stringify({
|
|
name: 'c',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
d: t.fixture('symlink', '../d'),
|
|
e: t.fixture('symlink', '../group/e'),
|
|
f: t.fixture('symlink', '../group/f'),
|
|
foo: {
|
|
'package.json': JSON.stringify({
|
|
name: 'foo',
|
|
version: '1.1.1',
|
|
dependencies: {
|
|
bar: '^1.0.0',
|
|
},
|
|
}),
|
|
},
|
|
bar: {
|
|
'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }),
|
|
},
|
|
baz: {
|
|
'package.json': JSON.stringify({ name: 'baz', version: '1.0.0' }),
|
|
},
|
|
pacote: {
|
|
'package.json': JSON.stringify({ name: 'pacote', version: '1.0.0' }),
|
|
},
|
|
},
|
|
a: {
|
|
'package.json': JSON.stringify({
|
|
name: 'a',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
c: '^1.0.0',
|
|
d: '^1.0.0',
|
|
},
|
|
devDependencies: {
|
|
baz: '^1.0.0',
|
|
},
|
|
}),
|
|
},
|
|
b: {
|
|
'package.json': JSON.stringify({
|
|
name: 'b',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
d: {
|
|
'package.json': JSON.stringify({
|
|
name: 'd',
|
|
version: '1.0.0',
|
|
dependencies: {
|
|
foo: '^1.1.1',
|
|
},
|
|
}),
|
|
},
|
|
group: {
|
|
e: {
|
|
'package.json': JSON.stringify({
|
|
name: 'e',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
f: {
|
|
'package.json': JSON.stringify({
|
|
name: 'f',
|
|
version: '1.0.0',
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
await sbom.exec(exec)
|
|
|
|
t.matchSnapshot(result())
|
|
}
|
|
|
|
t.test('should list workspaces properly with default configs', t => mockWorkspaces(t, [], {
|
|
'sbom-format': 'spdx',
|
|
}))
|
|
|
|
t.test('should not list workspaces with --no-workspaces', t => mockWorkspaces(t, [], {
|
|
'sbom-format': 'spdx',
|
|
workspaces: false,
|
|
}))
|
|
|
|
t.test('should filter worksapces with --workspace', t => mockWorkspaces(t, [], {
|
|
'sbom-format': 'spdx',
|
|
workspace: 'a',
|
|
}))
|
|
|
|
t.test('should filter workspaces with multiple --workspace flags', t => mockWorkspaces(t, [], {
|
|
'sbom-format': 'spdx',
|
|
workspace: ['e', 'f'],
|
|
}))
|
|
})
|
|
})
|