• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1
2const crypto = require('crypto')
3const normalizeData = require('normalize-package-data')
4const npa = require('npm-package-arg')
5const ssri = require('ssri')
6
7const SPDX_SCHEMA_VERSION = 'SPDX-2.3'
8const SPDX_DATA_LICENSE = 'CC0-1.0'
9const SPDX_IDENTIFER = 'SPDXRef-DOCUMENT'
10
11const NO_ASSERTION = 'NOASSERTION'
12
13const REL_DESCRIBES = 'DESCRIBES'
14const REL_PREREQ = 'PREREQUISITE_FOR'
15const REL_OPTIONAL = 'OPTIONAL_DEPENDENCY_OF'
16const REL_DEV = 'DEV_DEPENDENCY_OF'
17const REL_DEP = 'DEPENDENCY_OF'
18
19const REF_CAT_PACKAGE_MANAGER = 'PACKAGE-MANAGER'
20const REF_TYPE_PURL = 'purl'
21
22const spdxOutput = ({ npm, nodes, packageType }) => {
23  const rootNode = nodes.find(node => node.isRoot)
24  const childNodes = nodes.filter(node => !node.isRoot && !node.isLink)
25  const rootID = rootNode.pkgid
26  const uuid = crypto.randomUUID()
27  const ns = `http://spdx.org/spdxdocs/${npa(rootID).escapedName}-${rootNode.version}-${uuid}`
28
29  const relationships = []
30  const seen = new Set()
31  for (let node of nodes) {
32    if (node.isLink) {
33      node = node.target
34    }
35
36    if (seen.has(node)) {
37      continue
38    }
39    seen.add(node)
40
41    const rels = [...node.edgesOut.values()]
42      // Filter out edges that are linking to nodes not in the list
43      .filter(edge => nodes.find(n => n === edge.to))
44      .map(edge => toSpdxRelationship(node, edge))
45      .filter(rel => rel)
46
47    relationships.push(...rels)
48  }
49
50  const extraRelationships = nodes.filter(node => node.extraneous)
51    .map(node => toSpdxRelationship(rootNode, { to: node, type: 'optional' }))
52
53  relationships.push(...extraRelationships)
54
55  const bom = {
56    spdxVersion: SPDX_SCHEMA_VERSION,
57    dataLicense: SPDX_DATA_LICENSE,
58    SPDXID: SPDX_IDENTIFER,
59    name: rootID,
60    documentNamespace: ns,
61    creationInfo: {
62      created: new Date().toISOString(),
63      creators: [
64        `Tool: npm/cli-${npm.version}`,
65      ],
66    },
67    documentDescribes: [toSpdxID(rootNode)],
68    packages: [toSpdxItem(rootNode, { packageType }), ...childNodes.map(toSpdxItem)],
69    relationships: [
70      {
71        spdxElementId: SPDX_IDENTIFER,
72        relatedSpdxElement: toSpdxID(rootNode),
73        relationshipType: REL_DESCRIBES,
74      },
75      ...relationships,
76    ],
77  }
78
79  return bom
80}
81
82const toSpdxItem = (node, { packageType }) => {
83  normalizeData(node.package)
84
85  // Calculate purl from package spec
86  let spec = npa(node.pkgid)
87  spec = (spec.type === 'alias') ? spec.subSpec : spec
88  const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${node.resolved}` : '')
89
90  /* For workspace nodes, use the location from their linkNode */
91  let location = node.location
92  if (node.isWorkspace && node.linksIn.size > 0) {
93    location = node.linksIn.values().next().value.location
94  }
95
96  let license = node.package?.license
97  if (license) {
98    if (typeof license === 'object') {
99      license = license.type
100    }
101  }
102
103  const pkg = {
104    name: node.packageName,
105    SPDXID: toSpdxID(node),
106    versionInfo: node.version,
107    packageFileName: location,
108    description: node.package?.description || undefined,
109    primaryPackagePurpose: packageType ? packageType.toUpperCase() : undefined,
110    downloadLocation: (node.isLink ? undefined : node.resolved) || NO_ASSERTION,
111    filesAnalyzed: false,
112    homepage: node.package?.homepage || NO_ASSERTION,
113    licenseDeclared: license || NO_ASSERTION,
114    externalRefs: [
115      {
116        referenceCategory: REF_CAT_PACKAGE_MANAGER,
117        referenceType: REF_TYPE_PURL,
118        referenceLocator: purl,
119      },
120    ],
121  }
122
123  if (node.integrity) {
124    const integrity = ssri.parse(node.integrity, { single: true })
125    pkg.checksums = [{
126      algorithm: integrity.algorithm.toUpperCase(),
127      checksumValue: integrity.hexDigest(),
128    }]
129  }
130  return pkg
131}
132
133const toSpdxRelationship = (node, edge) => {
134  let type
135  switch (edge.type) {
136    case 'peer':
137      type = REL_PREREQ
138      break
139    case 'optional':
140      type = REL_OPTIONAL
141      break
142    case 'dev':
143      type = REL_DEV
144      break
145    default:
146      type = REL_DEP
147  }
148
149  return {
150    spdxElementId: toSpdxID(edge.to),
151    relatedSpdxElement: toSpdxID(node),
152    relationshipType: type,
153  }
154}
155
156const toSpdxID = (node) => {
157  let name = node.packageName
158
159  // Strip leading @ for scoped packages
160  name = name.replace(/^@/, '')
161
162  // Replace slashes with dots
163  name = name.replace(/\//g, '.')
164
165  return `SPDXRef-Package-${name}-${node.version}`
166}
167
168const isGitNode = (node) => {
169  if (!node.resolved) {
170    return
171  }
172
173  try {
174    const { type } = npa(node.resolved)
175    return type === 'git' || type === 'hosted'
176  } catch (err) {
177    /* istanbul ignore next */
178    return false
179  }
180}
181
182module.exports = { spdxOutput }
183