1const t = require('tap') 2const Ajv = require('ajv') 3const applyFormats = require('ajv-formats') 4const applyDraftFormats = require('ajv-formats-draft2019') 5const { cyclonedxOutput } = require('../../../lib/utils/sbom-cyclonedx.js') 6 7const FAKE_UUID = 'urn:uuid:00000000-0000-0000-0000-000000000000' 8 9t.cleanSnapshot = s => { 10 let sbom 11 try { 12 sbom = JSON.parse(s) 13 } catch (e) { 14 return s 15 } 16 17 sbom.serialNumber = FAKE_UUID 18 if (sbom.metadata) { 19 sbom.metadata.timestamp = '2020-01-01T00:00:00.000Z' 20 } 21 22 return JSON.stringify(sbom, null, 2) 23} 24 25const npm = { version: '10.0.0 ' } 26 27const rootPkg = { 28 author: 'Author', 29} 30 31const root = { 32 name: 'root', 33 packageName: 'root', 34 version: '1.0.0', 35 pkgid: 'root@1.0.0', 36 isRoot: true, 37 package: rootPkg, 38 location: '', 39 edgesOut: [], 40} 41 42const dep1 = { 43 name: 'dep1', 44 packageName: 'dep1', 45 version: '0.0.1', 46 pkgid: 'dep1@0.0.1', 47 package: {}, 48 location: 'node_modules/dep1', 49 edgesOut: [], 50} 51 52const dep2 = { 53 name: 'dep2', 54 packageName: 'dep2', 55 version: '0.0.2', 56 pkgid: 'npm@npm:dep2@0.0.2', 57 package: {}, 58 location: 'node_modules/dep2', 59 edgesOut: [{ to: dep1 }], 60} 61 62const dep2Link = { 63 name: 'dep2', 64 packageName: 'dep2', 65 version: '0.0.2', 66 pkgid: 'dep2@0.0.2', 67 package: {}, 68 location: 'node_modules/dep2', 69 edgesOut: [], 70 isLink: true, 71 target: dep2, 72} 73 74t.test('single node - application package type', t => { 75 const res = cyclonedxOutput({ npm, nodes: [root], packageType: 'application' }) 76 t.matchSnapshot(JSON.stringify(res)) 77 t.end() 78}) 79 80t.test('single node - package lock only', t => { 81 const res = cyclonedxOutput({ npm, nodes: [root], packageLockOnly: true }) 82 t.matchSnapshot(JSON.stringify(res)) 83 t.end() 84}) 85 86t.test('single node - optional ', t => { 87 const node = { ...root, optional: true } 88 const res = cyclonedxOutput({ npm, nodes: [node] }) 89 t.matchSnapshot(JSON.stringify(res)) 90 t.end() 91}) 92 93t.test('single node - with description', t => { 94 const pkg = { ...rootPkg, description: 'Package description' } 95 const node = { ...root, package: pkg } 96 const res = cyclonedxOutput({ npm, nodes: [node] }) 97 t.matchSnapshot(JSON.stringify(res)) 98 t.end() 99}) 100 101t.test('single node - with author object', t => { 102 const pkg = { ...rootPkg, author: { name: 'Arthur' } } 103 const node = { ...root, package: pkg } 104 const res = cyclonedxOutput({ npm, nodes: [node] }) 105 t.matchSnapshot(JSON.stringify(res)) 106 t.end() 107}) 108 109t.test('single node - with integrity', t => { 110 /* eslint-disable-next-line max-len */ 111 const node = { ...root, integrity: 'sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==' } 112 const res = cyclonedxOutput({ npm, nodes: [node] }) 113 t.matchSnapshot(JSON.stringify(res)) 114 t.end() 115}) 116 117t.test('single node - development', t => { 118 const node = { ...root, dev: true } 119 const res = cyclonedxOutput({ npm, nodes: [node] }) 120 t.matchSnapshot(JSON.stringify(res)) 121 t.end() 122}) 123 124t.test('single node - extraneous', t => { 125 const node = { ...root, extraneous: true } 126 const res = cyclonedxOutput({ npm, nodes: [node] }) 127 t.matchSnapshot(JSON.stringify(res)) 128 t.end() 129}) 130 131t.test('single node - bundled', t => { 132 const node = { ...root, inBundle: true } 133 const res = cyclonedxOutput({ npm, nodes: [node] }) 134 t.matchSnapshot(JSON.stringify(res)) 135 t.end() 136}) 137 138t.test('single node - private', t => { 139 const pkg = { ...rootPkg, private: true } 140 const node = { ...root, package: pkg } 141 const res = cyclonedxOutput({ npm, nodes: [node] }) 142 t.matchSnapshot(JSON.stringify(res)) 143 t.end() 144}) 145 146t.test('single node - with repository url', t => { 147 const pkg = { ...rootPkg, repository: { url: 'https://foo.bar' } } 148 const node = { ...root, package: pkg } 149 const res = cyclonedxOutput({ npm, nodes: [node] }) 150 t.matchSnapshot(JSON.stringify(res)) 151 t.end() 152}) 153 154t.test('single node - with homepage', t => { 155 const pkg = { ...rootPkg, homepage: 'https://foo.bar/README.md' } 156 const node = { ...root, package: pkg } 157 const res = cyclonedxOutput({ npm, nodes: [node] }) 158 t.matchSnapshot(JSON.stringify(res)) 159 t.end() 160}) 161 162t.test('single node - with issue tracker', t => { 163 const pkg = { ...rootPkg, bugs: { url: 'https://foo.bar/issues' } } 164 const node = { ...root, package: pkg } 165 const res = cyclonedxOutput({ npm, nodes: [node] }) 166 t.matchSnapshot(JSON.stringify(res)) 167 t.end() 168}) 169 170t.test('single node - with distribution url', t => { 171 const node = { ...root, resolved: 'https://registry.npmjs.org/root/-/root-1.0.0.tgz' } 172 const res = cyclonedxOutput({ npm, nodes: [node] }) 173 t.matchSnapshot(JSON.stringify(res)) 174 t.end() 175}) 176 177t.test('single node - with single license', t => { 178 const pkg = { ...rootPkg, license: 'ISC' } 179 const node = { ...root, package: pkg } 180 const res = cyclonedxOutput({ npm, nodes: [node] }) 181 t.matchSnapshot(JSON.stringify(res)) 182 t.end() 183}) 184 185t.test('single node - with license expression', t => { 186 const pkg = { ...rootPkg, license: '(MIT OR Apache-2.0)' } 187 const node = { ...root, package: pkg } 188 const res = cyclonedxOutput({ npm, nodes: [node] }) 189 t.matchSnapshot(JSON.stringify(res)) 190 t.end() 191}) 192 193t.test('single node - with license object', t => { 194 const pkg = { 195 ...rootPkg, 196 license: { 197 type: 'MIT', 198 url: 'http://github.com/kriskowal/q/raw/master/LICENSE', 199 }, 200 } 201 const node = { ...root, package: pkg } 202 const res = cyclonedxOutput({ npm, nodes: [node] }) 203 t.matchSnapshot(JSON.stringify(res)) 204 t.end() 205}) 206 207t.test('single node - from git url', t => { 208 const node = { ...root, type: 'git', resolved: 'https://github.com/foo/bar#1234' } 209 const res = cyclonedxOutput({ npm, nodes: [node] }) 210 t.matchSnapshot(JSON.stringify(res)) 211 t.end() 212}) 213 214t.test('single node - no package info', t => { 215 const node = { ...root, package: undefined } 216 const res = cyclonedxOutput({ npm, nodes: [node] }) 217 t.matchSnapshot(JSON.stringify(res)) 218 t.end() 219}) 220 221t.test('node - with deps', t => { 222 const node = { 223 ...root, 224 edgesOut: [ 225 { to: dep1 }, 226 { to: dep2 }, 227 { to: undefined }, 228 { to: { pkgid: 'foo' } }, 229 ], 230 } 231 const res = cyclonedxOutput({ npm, nodes: [node, dep1, dep2, dep2Link] }) 232 t.matchSnapshot(JSON.stringify(res)) 233 t.end() 234}) 235 236// Check that all of the generated test snapshots validate against the CycloneDX schema 237t.test('schema validation', t => { 238 // Load schemas 239 const cdxSchema = require('../../schemas/cyclonedx/bom-1.5.schema.json') 240 const spdxLicenseSchema = require('../../schemas/cyclonedx/spdx.schema.json') 241 const jsfSchema = require('../../schemas/cyclonedx/jsf-0.82.schema.json') 242 243 const ajv = new Ajv({ 244 strict: false, 245 schemas: [spdxLicenseSchema, jsfSchema, cdxSchema], 246 }) 247 applyFormats(ajv) 248 applyDraftFormats(ajv) 249 250 // Retrieve compiled schema 251 const validate = ajv.getSchema('http://cyclonedx.org/schema/bom-1.5.schema.json') 252 253 // Load snapshots for all tests in this file 254 const sboms = require('../../../tap-snapshots/test/lib/utils/sbom-cyclonedx.js.test.cjs') 255 256 // Check that all snapshots validate against the CycloneDX schema 257 Object.entries(sboms).forEach(([name, sbom]) => { 258 t.ok(validate(JSON.parse(sbom)), { snapshot: name, error: validate.errors?.[0] }) 259 }) 260 t.end() 261}) 262