codekingpro/portable-devtools
114k
1
2const crypto = require('node:crypto')
3const PackageJson = require('@npmcli/package-json')
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 // Create list of child nodes w/ unique IDs
30 const childNodeMap = new Map()
31 for (const item of childNodes) {
32 const id = toSpdxID(item)
33 if (!childNodeMap.has(id)) {
34 childNodeMap.set(id, item)
35 }
36 }
37 const uniqueChildNodes = Array.from(childNodeMap.values())
38
39 const relationships = []
40 const seen = new Set()
41 for (let node of nodes) {
42 if (node.isLink) {
43 node = node.target
44 }
45
46 if (seen.has(node)) {
47 continue
48 }
49 seen.add(node)
50
51 const rels = [...node.edgesOut.values()]
52 // Filter out edges that are linking to nodes not in the list
53 .filter(edge => nodes.find(n => n === edge.to))
54 .map(edge => toSpdxRelationship(node, edge))
55 .filter(rel => rel)
56
57 relationships.push(...rels)
58 }
59
60 const extraRelationships = nodes.filter(node => node.extraneous)
61 .map(node => toSpdxRelationship(rootNode, { to: node, type: 'optional' }))
62
63 relationships.push(...extraRelationships)
64
65 const bom = {
66 spdxVersion: SPDX_SCHEMA_VERSION,
67 dataLicense: SPDX_DATA_LICENSE,
68 SPDXID: SPDX_IDENTIFER,
69 name: rootID,
70 documentNamespace: ns,
71 creationInfo: {
72 created: new Date().toISOString(),
73 creators: [
74 `Tool: npm/cli-${npm.version}`,
75 ],
76 },
77 documentDescribes: [toSpdxID(rootNode)],
78 packages: [toSpdxItem(rootNode, { packageType }), ...uniqueChildNodes.map(toSpdxItem)],
79 relationships: [
80 {
81 spdxElementId: SPDX_IDENTIFER,
82 relatedSpdxElement: toSpdxID(rootNode),
83 relationshipType: REL_DESCRIBES,
84 },
85 ...relationships,
86 ],
87 }
88
89 return bom
90}
91
92const toSpdxItem = (node, { packageType }) => {
93 const toNormalize = new PackageJson()
94 toNormalize.fromContent(node.package).normalize({ steps: ['normalizeData'] })
95 node.package = toNormalize.content
96
97 // Calculate purl from package spec
98 let spec = npa(node.pkgid)
99 spec = (spec.type === 'alias') ? spec.subSpec : spec
100 const purl = npa.toPurl(spec) + (isGitNode(node) ? `?vcs_url=${node.resolved}` : '')
101
102 /* For workspace nodes, use the location from their linkNode */
103 let location = node.location
104 if (node.isWorkspace && node.linksIn.size > 0) {
105 location = node.linksIn.values().next().value.location
106 }
107
108 let license = node.package?.license
109 if (license) {
110 if (typeof license === 'object') {
111 license = license.type
112 }
113 } else if (Array.isArray(node.package?.licenses)) {
114 license = node.package.licenses
115 .map(l => (typeof l === 'object' ? l.type : l))
116 .filter(Boolean)
117 .join(' OR ')
118 }
119
120 const pkg = {
121 name: node.packageName,
122 SPDXID: toSpdxID(node),
123 versionInfo: node.version,
124 packageFileName: location,
125 description: node.package?.description || undefined,
126 primaryPackagePurpose: packageType ? packageType.toUpperCase() : undefined,
127 downloadLocation: (node.isLink ? undefined : node.resolved) || NO_ASSERTION,
128 filesAnalyzed: false,
129 homepage: node.package?.homepage || NO_ASSERTION,
130 licenseDeclared: license || NO_ASSERTION,
131 externalRefs: [
132 {
133 referenceCategory: REF_CAT_PACKAGE_MANAGER,
134 referenceType: REF_TYPE_PURL,
135 referenceLocator: purl,
136 },
137 ],
138 }
139
140 if (node.integrity) {
141 const integrity = ssri.parse(node.integrity, { single: true })
142 pkg.checksums = [{
143 algorithm: integrity.algorithm.toUpperCase(),
144 checksumValue: integrity.hexDigest(),
145 }]
146 }
147 return pkg
148}
149
150const toSpdxRelationship = (node, edge) => {
151 let type
152 switch (edge.type) {
153 case 'peer':
154 type = REL_PREREQ
155 break
156 case 'optional':
157 type = REL_OPTIONAL
158 break
159 case 'dev':
160 type = REL_DEV
161 break
162 default:
163 type = REL_DEP
164 }
165
166 return {
167 spdxElementId: toSpdxID(edge.to),
168 relatedSpdxElement: toSpdxID(node),
169 relationshipType: type,
170 }
171}
172
173const toSpdxID = (node) => {
174 let name = node.packageName
175
176 // Strip leading @ for scoped packages
177 name = name.replace(/^@/, '')
178
179 // Replace slashes with dots
180 name = name.replace(/\//g, '.')
181
182 return `SPDXRef-Package-${name}-${node.version}`
183}
184
185const isGitNode = (node) => {
186 if (!node.resolved) {
187 return
188 }
189
190 try {
191 const { type } = npa(node.resolved)
192 return type === 'git' || type === 'hosted'
193 } catch {
194 /* istanbul ignore next */
195 return false
196 }
197}
198
199module.exports = { spdxOutput }
200 