Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
sbom-spdx.js200 linesDownload Raw Back to utils
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 
codekingpro/portable-devtools · Team Ai