1const t = require('tap') 2const mockNpm = require('../../fixtures/mock-npm.js') 3 4const FAKE_TIMESTAMP = '2020-01-01T00:00:00.000Z' 5const FAKE_UUID = '00000000-0000-0000-0000-000000000000' 6 7t.cleanSnapshot = s => { 8 let sbom 9 10 try { 11 sbom = JSON.parse(s) 12 } catch (e) { 13 return s 14 } 15 16 // Clean dynamic values from snapshots. SPDX and CycloneDX have different 17 // formats for these values, so we need to do it separately. 18 if (sbom.SPDXID) { 19 sbom.documentNamespace = `http://spdx.org/spdxdocs/test-npm-sbom-1.0.0-${FAKE_UUID}` 20 21 if (sbom.creationInfo) { 22 sbom.creationInfo.created = FAKE_TIMESTAMP 23 sbom.creationInfo.creators = ['Tool: npm/cli-10.0.0'] 24 } 25 } else { 26 sbom.serialNumber = `urn:uuid:${FAKE_UUID}` 27 28 if (sbom.metadata) { 29 sbom.metadata.timestamp = FAKE_TIMESTAMP 30 sbom.metadata.tools[0].version = '10.0.0' 31 } 32 } 33 34 return JSON.stringify(sbom, null, 2) 35} 36 37const simpleNmFixture = { 38 node_modules: { 39 foo: { 40 'package.json': JSON.stringify({ 41 name: 'foo', 42 version: '1.0.0', 43 dependencies: { 44 dog: '^1.0.0', 45 }, 46 }), 47 node_modules: { 48 dog: { 49 'package.json': JSON.stringify({ 50 name: 'dog', 51 version: '1.0.0', 52 }), 53 }, 54 }, 55 }, 56 chai: { 57 'package.json': JSON.stringify({ 58 name: 'chai', 59 version: '1.0.0', 60 }), 61 }, 62 }, 63} 64 65const mockSbom = async (t, { mocks, config, ...opts } = {}) => { 66 const mock = await mockNpm(t, { 67 ...opts, 68 config: { 69 ...config, 70 }, 71 command: 'sbom', 72 mocks: { 73 path: { 74 ...require('path'), 75 sep: '/', 76 }, 77 ...mocks, 78 }, 79 }) 80 81 return { 82 ...mock, 83 result: () => mock.joinedOutput(), 84 } 85} 86 87t.test('sbom', async t => { 88 t.test('basic sbom - spdx', async t => { 89 const config = { 90 'sbom-format': 'spdx', 91 } 92 const { result, sbom } = await mockSbom(t, { 93 config, 94 prefixDir: { 95 'package.json': JSON.stringify({ 96 name: 'test-npm-sbom', 97 version: '1.0.0', 98 dependencies: { 99 foo: '^1.0.0', 100 chai: '^1.0.0', 101 }, 102 }), 103 ...simpleNmFixture, 104 }, 105 }) 106 await sbom.exec([]) 107 t.matchSnapshot(result()) 108 }) 109 110 t.test('basic sbom - cyclonedx', async t => { 111 const config = { 112 'sbom-format': 'cyclonedx', 113 'sbom-type': 'application', 114 } 115 const { result, sbom } = await mockSbom(t, { 116 config, 117 prefixDir: { 118 'package.json': JSON.stringify({ 119 name: 'test-npm-sbom', 120 version: '1.0.0', 121 dependencies: { 122 foo: '^1.0.0', 123 chai: '^1.0.0', 124 }, 125 }), 126 ...simpleNmFixture, 127 }, 128 }) 129 await sbom.exec([]) 130 t.matchSnapshot(result()) 131 }) 132 133 t.test('--omit dev', async t => { 134 const config = { 135 'sbom-format': 'spdx', 136 omit: ['dev'], 137 } 138 const { result, sbom } = await mockSbom(t, { 139 config, 140 prefixDir: { 141 'package.json': JSON.stringify({ 142 name: 'test-npm-sbom', 143 version: '1.0.0', 144 dependencies: { 145 foo: '^1.0.0', 146 }, 147 devDependencies: { 148 chai: '^1.0.0', 149 }, 150 }), 151 ...simpleNmFixture, 152 }, 153 }) 154 await sbom.exec([]) 155 t.matchSnapshot(result()) 156 }) 157 158 t.test('--omit optional', async t => { 159 const config = { 160 'sbom-format': 'spdx', 161 omit: ['optional'], 162 } 163 const { result, sbom } = await mockSbom(t, { 164 config, 165 prefixDir: { 166 'package.json': JSON.stringify({ 167 name: 'test-npm-sbom', 168 version: '1.0.0', 169 dependencies: { 170 chai: '^1.0.0', 171 }, 172 optionalDependencies: { 173 foo: '^1.0.0', 174 }, 175 }), 176 ...simpleNmFixture, 177 }, 178 }) 179 await sbom.exec([]) 180 t.matchSnapshot(result()) 181 }) 182 183 t.test('--omit peer', async t => { 184 const config = { 185 'sbom-format': 'spdx', 186 omit: ['peer'], 187 } 188 const { result, sbom } = await mockSbom(t, { 189 config, 190 prefixDir: { 191 'package.json': JSON.stringify({ 192 name: 'test-npm-sbom', 193 version: '1.0.0', 194 dependencies: { 195 chai: '^1.0.0', 196 }, 197 peerDependencies: { 198 foo: '^1.0.0', 199 }, 200 }), 201 ...simpleNmFixture, 202 }, 203 }) 204 await sbom.exec([]) 205 t.matchSnapshot(result()) 206 }) 207 208 t.test('missing format', async t => { 209 const config = {} 210 const { result, sbom } = await mockSbom(t, { 211 config, 212 prefixDir: { 213 'package.json': JSON.stringify({ 214 name: 'test-npm-sbom', 215 version: '1.0.0', 216 dependencies: { 217 foo: '^1.0.0', 218 chai: '^1.0.0', 219 }, 220 }), 221 ...simpleNmFixture, 222 }, 223 }) 224 await t.rejects(sbom.exec([]), { 225 code: 'EUSAGE', 226 message: 'Must specify --sbom-format flag with one of: cyclonedx, spdx.', 227 }, 228 'should throw error') 229 230 t.matchSnapshot(result()) 231 }) 232 233 t.test('invalid dep', async t => { 234 const config = { 235 'sbom-format': 'spdx', 236 } 237 const { sbom } = await mockSbom(t, { 238 config, 239 prefixDir: { 240 'package.json': JSON.stringify({ 241 name: 'test-npm-ls', 242 version: '1.0.0', 243 dependencies: { 244 foo: '^2.0.0', 245 }, 246 }), 247 ...simpleNmFixture, 248 }, 249 }) 250 await t.rejects( 251 sbom.exec([]), 252 { code: 'ESBOMPROBLEMS', message: /invalid: foo@1.0.0/ }, 253 'should list dep problems' 254 ) 255 }) 256 257 t.test('missing dep', async t => { 258 const config = { 259 'sbom-format': 'spdx', 260 } 261 const { sbom } = await mockSbom(t, { 262 config, 263 prefixDir: { 264 'package.json': JSON.stringify({ 265 name: 'test-npm-ls', 266 version: '1.0.0', 267 dependencies: { 268 ipsum: '^1.0.0', 269 }, 270 }), 271 ...simpleNmFixture, 272 }, 273 }) 274 await t.rejects( 275 sbom.exec([]), 276 { code: 'ESBOMPROBLEMS', message: /missing: ipsum@\^1.0.0/ }, 277 'should list dep problems' 278 ) 279 }) 280 281 t.test('missing (optional) dep', async t => { 282 const config = { 283 'sbom-format': 'spdx', 284 } 285 const { result, sbom } = await mockSbom(t, { 286 config, 287 prefixDir: { 288 'package.json': JSON.stringify({ 289 name: 'test-npm-ls', 290 version: '1.0.0', 291 dependencies: { 292 foo: '^1.0.0', 293 chai: '^1.0.0', 294 }, 295 optionalDependencies: { 296 ipsum: '^1.0.0', 297 }, 298 }), 299 ...simpleNmFixture, 300 }, 301 }) 302 await sbom.exec([]) 303 t.matchSnapshot(result()) 304 }) 305 306 t.test('extraneous dep', async t => { 307 const config = { 308 'sbom-format': 'spdx', 309 } 310 const { result, sbom } = await mockSbom(t, { 311 config, 312 prefixDir: { 313 'package.json': JSON.stringify({ 314 name: 'test-npm-ls', 315 version: '1.0.0', 316 dependencies: { 317 foo: '^1.0.0', 318 }, 319 }), 320 ...simpleNmFixture, 321 }, 322 }) 323 await sbom.exec([]) 324 t.matchSnapshot(result()) 325 }) 326 327 t.test('lock file only', async t => { 328 const config = { 329 'sbom-format': 'spdx', 330 'package-lock-only': true, 331 } 332 const { result, sbom } = await mockSbom(t, { 333 config, 334 prefixDir: { 335 'package.json': JSON.stringify({ 336 name: 'test-npm-ls', 337 version: '1.0.0', 338 dependencies: { 339 foo: '^1.0.0', 340 chai: '^1.0.0', 341 }, 342 }), 343 'package-lock.json': JSON.stringify({ 344 dependencies: { 345 foo: { 346 version: '1.0.0', 347 requires: { 348 dog: '^1.0.0', 349 }, 350 }, 351 dog: { 352 version: '1.0.0', 353 }, 354 chai: { 355 version: '1.0.0', 356 }, 357 }, 358 }), 359 }, 360 }) 361 await sbom.exec([]) 362 t.matchSnapshot(result()) 363 }) 364 365 t.test('lock file only - missing lock file', async t => { 366 const config = { 367 'sbom-format': 'spdx', 368 'package-lock-only': true, 369 } 370 const { result, sbom } = await mockSbom(t, { 371 config, 372 prefixDir: { 373 'package.json': JSON.stringify({ 374 name: 'test-npm-ls', 375 version: '1.0.0', 376 dependencies: { 377 foo: '^1.0.0', 378 chai: '^1.0.0', 379 }, 380 }), 381 }, 382 }) 383 await t.rejects(sbom.exec([]), { 384 code: 'EUSAGE', 385 message: 'A package lock or shrinkwrap file is required in package-lock-only mode', 386 }, 387 'should throw error') 388 389 t.matchSnapshot(result()) 390 }) 391 392 t.test('loading a tree containing workspaces', async t => { 393 const mockWorkspaces = async (t, exec = [], config = {}) => { 394 const { result, sbom } = await mockSbom(t, { 395 config, 396 prefixDir: { 397 'package.json': JSON.stringify({ 398 name: 'workspaces-tree', 399 version: '1.0.0', 400 workspaces: ['./a', './b', './d', './group/*'], 401 dependencies: { pacote: '1.0.0' }, 402 }), 403 node_modules: { 404 a: t.fixture('symlink', '../a'), 405 b: t.fixture('symlink', '../b'), 406 c: { 407 'package.json': JSON.stringify({ 408 name: 'c', 409 version: '1.0.0', 410 }), 411 }, 412 d: t.fixture('symlink', '../d'), 413 e: t.fixture('symlink', '../group/e'), 414 f: t.fixture('symlink', '../group/f'), 415 foo: { 416 'package.json': JSON.stringify({ 417 name: 'foo', 418 version: '1.1.1', 419 dependencies: { 420 bar: '^1.0.0', 421 }, 422 }), 423 }, 424 bar: { 425 'package.json': JSON.stringify({ name: 'bar', version: '1.0.0' }), 426 }, 427 baz: { 428 'package.json': JSON.stringify({ name: 'baz', version: '1.0.0' }), 429 }, 430 pacote: { 431 'package.json': JSON.stringify({ name: 'pacote', version: '1.0.0' }), 432 }, 433 }, 434 a: { 435 'package.json': JSON.stringify({ 436 name: 'a', 437 version: '1.0.0', 438 dependencies: { 439 c: '^1.0.0', 440 d: '^1.0.0', 441 }, 442 devDependencies: { 443 baz: '^1.0.0', 444 }, 445 }), 446 }, 447 b: { 448 'package.json': JSON.stringify({ 449 name: 'b', 450 version: '1.0.0', 451 }), 452 }, 453 d: { 454 'package.json': JSON.stringify({ 455 name: 'd', 456 version: '1.0.0', 457 dependencies: { 458 foo: '^1.1.1', 459 }, 460 }), 461 }, 462 group: { 463 e: { 464 'package.json': JSON.stringify({ 465 name: 'e', 466 version: '1.0.0', 467 }), 468 }, 469 f: { 470 'package.json': JSON.stringify({ 471 name: 'f', 472 version: '1.0.0', 473 }), 474 }, 475 }, 476 }, 477 }) 478 479 await sbom.exec(exec) 480 481 t.matchSnapshot(result()) 482 } 483 484 t.test('should list workspaces properly with default configs', t => mockWorkspaces(t, [], { 485 'sbom-format': 'spdx', 486 })) 487 488 t.test('should not list workspaces with --no-workspaces', t => mockWorkspaces(t, [], { 489 'sbom-format': 'spdx', 490 workspaces: false, 491 })) 492 493 t.test('should filter worksapces with --workspace', t => mockWorkspaces(t, [], { 494 'sbom-format': 'spdx', 495 workspace: 'a', 496 })) 497 498 t.test('should filter workspaces with multiple --workspace flags', t => mockWorkspaces(t, [], { 499 'sbom-format': 'spdx', 500 workspace: ['e', 'f'], 501 })) 502 }) 503}) 504