diff --git a/package.json b/package.json index 3a64cd18..9c6f6caf 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,11 @@ ], "dependencies": { "@cyclonedx/cyclonedx-library": "^6.8.0", - "@yarnpkg/cli": "^4.1.0", - "@yarnpkg/core": "^4.0.3", - "@yarnpkg/fslib": "^3.0.2", + "@yarnpkg/cli": "^4", + "@yarnpkg/core": "^4", + "@yarnpkg/fslib": "^3", + "@yarnpkg/plugin-git": "^3", + "@yarnpkg/plugin-github": "^3", "clipanion": "^4.0.0-rc.3", "normalize-package-data": "^3||^4||^5||^6", "packageurl-js": "^1.2.1", diff --git a/src/builders.ts b/src/builders.ts index b8467d90..9991a5a7 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -19,9 +19,9 @@ Copyright (c) OWASP Foundation. All Rights Reserved. // import submodules so to prevent load of unused not-tree-shakable dependencies - like 'AJV' import type { FromNodePackageJson as PJB } from '@cyclonedx/cyclonedx-library/Builders' -import { ComponentType, LicenseAcknowledgement } from '@cyclonedx/cyclonedx-library/Enums' +import { ComponentType, ExternalReferenceType, LicenseAcknowledgement } from '@cyclonedx/cyclonedx-library/Enums' import type { FromNodePackageJson as PJF } from '@cyclonedx/cyclonedx-library/Factories' -import { Bom, Component, type License, Property, type Tool } from '@cyclonedx/cyclonedx-library/Models' +import { Bom, Component, ExternalReference, type License, Property, type Tool } from '@cyclonedx/cyclonedx-library/Models' import { BomUtility } from '@cyclonedx/cyclonedx-library/Utils' import { Cache, type FetchOptions, type Locator, type LocatorHash, type Package, type Project, structUtils, ThrowReport, type Workspace } from '@yarnpkg/core' import { ppath } from '@yarnpkg/fslib' @@ -30,6 +30,7 @@ import type { PackageURL } from 'packageurl-js' import { getBuildtimeInfo } from './_buildtimeInfo' import { isString } from './_helpers' +import { getPackageSource } from './evidence' import { PropertyNames, PropertyValueBool } from './properties' type ManifestFetcher = (pkg: Package) => Promise @@ -193,6 +194,15 @@ export class BomBuilder { return undefined } + const packageSource = getPackageSource(locator) + if (packageSource !== undefined) { + if (packageSource instanceof URL || packageSource.length > 0) { + component.externalReferences.add( + new ExternalReference(packageSource, ExternalReferenceType.Distribution) + ) + } + } + // even private packages may have a PURL for identification component.purl = this.makePurl(component) diff --git a/src/evidence.ts b/src/evidence.ts new file mode 100644 index 00000000..a920bc6b --- /dev/null +++ b/src/evidence.ts @@ -0,0 +1,107 @@ +/*! +This file is part of CycloneDX SBOM plugin for yarn. + +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. +*/ + +// import submodules so to prevent load of unused not-tree-shakable dependencies - like 'AJV' +import { type Locator, structUtils } from '@yarnpkg/core' +import { gitUtils as YarnPluginGitUtils } from '@yarnpkg/plugin-git' +import { githubUtils as YarnPluginGithubUtils } from '@yarnpkg/plugin-github' + +import { isString } from './_helpers' + +/** + * Scope is to detect package sources. + * But unlike the actual YarnResolvers, not resolve them, but find evidence for the actually used ones. + */ +export function getPackageSource (locator: Locator): URL | string | undefined { + for (const candidate of packageSourceCandidates) { + const source = candidate(locator) + if (source !== false && source !== undefined) { + return source + } + } + return undefined +} + +type PackageSourceCandidate = (locator: Locator) => URL | string | false | undefined + +const packageSourceCandidates: PackageSourceCandidate[] = [ + function /* workspace */ (locator: Locator): string | false | undefined { + if (!locator.reference.startsWith('workspace:')) { + return false + } + // TODO implement + // see https://github.com/yarnpkg/berry/tree/master/packages/plugin-file + return undefined + }, + function /* npm: */ (locator: Locator): string | false | undefined { + if (!locator.reference.startsWith('npm:')) { + return false + } + // see https://github.com/yarnpkg/berry/blob/bfa6489467e0e11ee87268e01e38e4f7e8d4d4b0/packages/plugin-npm/sources/NpmHttpFetcher.ts#L51 + const { params } = structUtils.parseRange(locator.reference) + if (params !== null && isString(params.__archiveUrl)) { + return params.__archiveUrl + } + // for range and remap there are no concrete evidence how the resolution was done on install-time. + // therefore, return undefined, for now ... + return undefined + }, + function /* github */ (locator: Locator): string | false { + if (!YarnPluginGithubUtils.isGithubUrl(locator.reference)) { + return false + } + // TODO sanitize & remove secrets + return locator.reference + }, + function /* git */ (locator: Locator): string | false { + if (!YarnPluginGitUtils.isGitUrl(locator.reference)) { + return false + } + // TODO sanitize & remove secrets + return locator.reference + }, + function /* https */ (locator: Locator): URL | false | undefined { + // see https://github.com/yarnpkg/berry/blob/bfa6489467e0e11ee87268e01e38e4f7e8d4d4b0/packages/plugin-http/sources/urlUtils.ts#L9 + if (!locator.reference.startsWith('http:') && !locator.reference.startsWith('https:')) { + return false + } + try { + // TODO sanitize & remove secrets + return new URL(locator.reference) + } catch { + return undefined // invalid URL + } + }, + function /* link | portal */ (locator: Locator): false | undefined { + if (!locator.reference.startsWith('link:') && !locator.reference.startsWith('portal:')) { + return false + } + // TODO: resolve path relative to current workspace + // see https://github.com/yarnpkg/berry/tree/master/packages/plugin-link + return undefined + }, + function /* file */ (locator: Locator): false | undefined { + if (!locator.reference.startsWith('file:')) { + return false + } + // TODO: resolve path relative to current workspace + // see https://github.com/yarnpkg/berry/tree/master/packages/plugin-file + return undefined + } +] diff --git a/tests/integration/index.test.js b/tests/integration/index.test.js index 39c2be3e..800526ca 100644 --- a/tests/integration/index.test.js +++ b/tests/integration/index.test.js @@ -43,7 +43,7 @@ const { const testSetups = [ /* region functional tests */ - 'alternative-package-registry', + 'alternative-package-registry' /* 'bundled-dependencies', 'concurrent-versions', 'dev-dependencies',