UNPKG

@cyclonedx/cyclonedx-library

Version:

Core functionality of CycloneDX for JavaScript (Node.js or WebBrowser).

185 lines (166 loc) 7.93 kB
/*! This file is part of CycloneDX JavaScript Library. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ /** * Node-specifics. * * Intended to run on normalized data structures * based on [PackageJson spec](https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/package.json) * and explained by [PackageJson description](https://docs.npmjs.com/cli/v9/configuring-npm/package-json). * Normalization should be done downstream, for example via [`normalize-package-data`](https://www.npmjs.com/package/normalize-package-data). */ import type { PackageURL } from 'packageurl-js' import { PurlQualifierNames } from 'packageurl-js' import { tryCanonicalizeGitUrl } from "../_helpers/gitUrl" import { isNotUndefined } from '../_helpers/notUndefined' import type { PackageJson } from '../_helpers/packageJson' import { ExternalReferenceType } from '../enums/externalReferenceType' import { HashAlgorithm } from "../enums/hashAlogorithm"; import type { Component } from '../models/component' import { ExternalReference } from '../models/externalReference' import { HashDictionary } from '../models/hash' import { defaultRegistryMatcher, parsePackageIntegrity } from '../utils/npmjsUtility.node' import { PackageUrlFactory as PlainPackageUrlFactory } from './packageUrl' /** * Node-specific ExternalReferenceFactory. */ export class ExternalReferenceFactory { makeExternalReferences (data: PackageJson): ExternalReference[] { const refs: Array<ExternalReference | undefined> = [] try { refs.push(this.makeVcs(data)) } catch { /* pass */ } try { refs.push(this.makeHomepage(data)) } catch { /* pass */ } try { refs.push(this.makeIssueTracker(data)) } catch { /* pass */ } try { refs.push(this.makeDist(data)) } catch { /* pass */ } return refs.filter(isNotUndefined) } makeVcs (data: PackageJson): ExternalReference | undefined { /* see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#repositoryc */ const repository = data.repository let url = undefined let comment: string | undefined = undefined if (typeof repository === 'object') { url = tryCanonicalizeGitUrl(repository.url) comment = 'as detected from PackageJson property "repository.url"' if (typeof repository.directory === 'string' && url instanceof URL) { // node does not properly encode `#` in the hash ... need to manually esscape url.hash = repository.directory.replace(/#/g, '%23') comment += ' and "repository.directory"' } } else { url = tryCanonicalizeGitUrl(repository) comment = 'as detected from PackageJson property "repository"' } return url === undefined ? undefined // cast to string so the URL is frozen/immutable : new ExternalReference(url.toString(), ExternalReferenceType.VCS, { comment }) } makeHomepage (data: PackageJson): ExternalReference | undefined { /* see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#homepage */ const url = data.homepage return typeof url === 'string' && url.length > 0 ? new ExternalReference( url, ExternalReferenceType.Website, { comment: 'as detected from PackageJson property "homepage"' }) : undefined } makeIssueTracker (data: PackageJson): ExternalReference | undefined { /* see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#bugs */ const bugs = data.bugs let url = undefined let comment: string | undefined = undefined if (typeof bugs === 'object') { url = bugs.url comment = 'as detected from PackageJson property "bugs.url"' } else { url = bugs comment = 'as detected from PackageJson property "bugs"' } return typeof url === 'string' && url.length > 0 ? new ExternalReference(url, ExternalReferenceType.IssueTracker, { comment }) : undefined } makeDist(data: PackageJson): ExternalReference | undefined { // "dist" might be used in bundled dependencies' manifests. // docs: https://blog.npmjs.org/post/172999548390/new-pgp-machinery /* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- acknowledged */ const { tarball, integrity, shasum } = data.dist ?? {} if (typeof tarball === 'string') { const hashes = new HashDictionary() let comment = 'as detected from PackageJson property "dist.tarball"' if (typeof integrity === 'string') { try { // actually not the hash of the file, but more of an integrity-check -- lets use it anyway. // see https://blog.npmjs.org/post/172999548390/new-pgp-machinery hashes.set(...parsePackageIntegrity(integrity)) comment += ' and property "dist.integrity"' } catch { /* pass */ } } if (typeof shasum === 'string' && shasum.length === 40) { hashes.set(HashAlgorithm["SHA-1"], shasum) comment += ' and property "dist.shasum"' } return new ExternalReference(tarball, ExternalReferenceType.Distribution, { hashes, comment }) } return undefined } } /** * Node-specific PackageUrlFactory. * @see {@link https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm} */ export class PackageUrlFactory extends PlainPackageUrlFactory<'npm'> { /* eslint-disable-next-line @typescript-eslint/no-inferrable-types -- docs */ override makeFromComponent (component: Component, sort: boolean = false): PackageURL | undefined { const purl = super.makeFromComponent(component, sort) return purl === undefined ? undefined : this.#finalizeQualifiers(purl) } /** * Will strip unnecessary qualifiers according to [PURL-SPECIFICATION](https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs). * <blockquote cite="https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs"> * Do not abuse qualifiers: it can be tempting to use many qualifier keys but their usage should be limited * to the bare minimum for proper package identification to ensure that a purl stays compact and readable * in most cases. * </blockquote> * * Therefore: * - "vcs_url" is stripped, if a "download_url" is given. * - "download_url" is stripped, if it is NPM's default registry ("registry.npmjs.org") * - "checksum" is stripped, unless a "download_url" or "vcs_url" is given. */ #finalizeQualifiers(purl: PackageURL): PackageURL { const qualifiers = new Map(Object.entries(purl.qualifiers ?? {})) const downloadUrl = qualifiers.get(PurlQualifierNames.DownloadUrl) if (downloadUrl !== undefined) { qualifiers.delete(PurlQualifierNames.VcsUrl) if (defaultRegistryMatcher.test(downloadUrl)) { qualifiers.delete(PurlQualifierNames.DownloadUrl) } } if (!qualifiers.has(PurlQualifierNames.DownloadUrl) && !qualifiers.has(PurlQualifierNames.VcsUrl)) { // nothing to base a checksum on qualifiers.delete(PurlQualifierNames.Checksum) } if (qualifiers.size > 0) { purl.qualifiers = Object.fromEntries(qualifiers.entries()) /* @ts-expect-error TS2322 */ purl.qualifiers.__proto__ = null /* eslint-disable-line no-proto -- intended */ } else { purl.qualifiers = undefined } return purl } }