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