File Explorer

/var/lang/lib/node_modules/npm/lib/utils

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

sbom-cyclonedx.js5.8 KB · 211 lines
const crypto = require('node:crypto')const parseLicense = require('spdx-expression-parse')const PackageJson = require('@npmcli/package-json')const npa = require('npm-package-arg')const ssri = require('ssri') const CYCLONEDX_SCHEMA = 'http://cyclonedx.org/schema/bom-1.5.schema.json'const CYCLONEDX_FORMAT = 'CycloneDX'const CYCLONEDX_SCHEMA_VERSION = '1.5' const PROP_BUNDLED = 'cdx:npm:package:bundled'const PROP_DEVELOPMENT = 'cdx:npm:package:development'const PROP_EXTRANEOUS = 'cdx:npm:package:extraneous'const PROP_PRIVATE = 'cdx:npm:package:private' const REF_VCS = 'vcs'const REF_WEBSITE = 'website'const REF_ISSUE_TRACKER = 'issue-tracker'const REF_DISTRIBUTION = 'distribution' const ALGO_MAP = {  sha1: 'SHA-1',  sha256: 'SHA-256',  sha384: 'SHA-384',  sha512: 'SHA-512',} const cyclonedxOutput = ({ npm, nodes, packageType, packageLockOnly }) => {  const rootNode = nodes.find(node => node.isRoot)  const childNodes = nodes.filter(node => !node.isRoot && !node.isLink)  const uuid = crypto.randomUUID()   // Create list of child nodes w/ unique IDs  const childNodeMap = new Map()  for (const item of childNodes) {    const id = toCyclonedxID(item)    if (!childNodeMap.has(id)) {      childNodeMap.set(id, item)    }  }  const uniqueChildNodes = Array.from(childNodeMap.values())   const deps = [rootNode, ...uniqueChildNodes]    .map(node => toCyclonedxDependency(node, nodes))   const bom = {    $schema: CYCLONEDX_SCHEMA,    bomFormat: CYCLONEDX_FORMAT,    specVersion: CYCLONEDX_SCHEMA_VERSION,    serialNumber: `urn:uuid:${uuid}`,    version: 1,    metadata: {      timestamp: new Date().toISOString(),      lifecycles: [        { phase: packageLockOnly ? 'pre-build' : 'build' },      ],      tools: [        {          vendor: 'npm',          name: 'cli',          version: npm.version,        },      ],      component: toCyclonedxItem(rootNode, { packageType }),    },    components: uniqueChildNodes.map(toCyclonedxItem),    dependencies: deps,  }   return bom} const toCyclonedxItem = (node, { packageType }) => {  packageType = packageType || 'library'   // 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}` : '')   if (node.package) {    const toNormalize = new PackageJson()    toNormalize.fromContent(node.package).normalize({ steps: ['normalizeData'] })    node.package = toNormalize.content  }   let license = node.package?.license  if (license) {    if (typeof license === 'object') {      license = license.type    }  } else if (Array.isArray(node.package?.licenses)) {    license = node.package.licenses      .map(l => (typeof l === 'object' ? l.type : l))      .filter(Boolean)      .join(' OR ')  }   let parsedLicense  try {    parsedLicense = parseLicense(license)  } catch {    parsedLicense = null  }   const component = {    'bom-ref': toCyclonedxID(node),    type: packageType,    name: node.name,    version: node.version,    scope: (node.optional || node.devOptional) ? 'optional' : 'required',    author: (typeof node.package?.author === 'object')      ? node.package.author.name      : (node.package?.author || undefined),    description: node.package?.description || undefined,    purl: purl,    properties: [],    externalReferences: [],  }   if (node.integrity) {    const integrity = ssri.parse(node.integrity, { single: true })    component.hashes = [{      alg: ALGO_MAP[integrity.algorithm] || /* istanbul ignore next */ 'SHA-512',      content: integrity.hexDigest(),    }]  }   if (node.dev === true) {    component.properties.push(prop(PROP_DEVELOPMENT))  }   if (node.package?.private === true) {    component.properties.push(prop(PROP_PRIVATE))  }   if (node.extraneous === true) {    component.properties.push(prop(PROP_EXTRANEOUS))  }   if (node.inBundle === true) {    component.properties.push(prop(PROP_BUNDLED))  }   if (!node.isLink && node.resolved) {    component.externalReferences.push(extRef(REF_DISTRIBUTION, node.resolved))  }   if (node.package?.repository?.url) {    component.externalReferences.push(extRef(REF_VCS, node.package.repository.url))  }   if (node.package?.homepage) {    component.externalReferences.push(extRef(REF_WEBSITE, node.package.homepage))  }   if (node.package?.bugs?.url) {    component.externalReferences.push(extRef(REF_ISSUE_TRACKER, node.package.bugs.url))  }   // If license is a single SPDX license, use the license field  if (parsedLicense?.license) {    component.licenses = [{ license: { id: parsedLicense.license } }]    // If license is a conjunction, use the expression field  } else if (parsedLicense?.conjunction) {    component.licenses = [{ expression: license }]  }   return component} const toCyclonedxDependency = (node, nodes) => {  // A node can have multiple outgoing edges resolving to the same  // `name@version` (e.g. via npm aliases like `foo: npm:bar@1` alongside a  // direct `bar: ^1` dep), which would produce duplicate entries in  // `dependsOn`. CycloneDX 1.5 requires unique items, so dedupe by ref.  const dependsOn = [...new Set(    [...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 => toCyclonedxID(edge.to))      .filter(id => id)  )]  return {    ref: toCyclonedxID(node),    dependsOn,  }} const toCyclonedxID = (node) => `${node.packageName}@${node.version}` const prop = (name) => ({ name, value: 'true' }) const extRef = (type, url) => ({ type, url }) const isGitNode = (node) => {  if (!node.resolved) {    return  }   try {    const { type } = npa(node.resolved)    return type === 'git' || type === 'hosted'  } catch {    /* istanbul ignore next */    return false  }} module.exports = { cyclonedxOutput }