diff --git a/README.md b/README.md index e16bbac4..18aa9d34 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ $ yarn cyclonedx ━━━ Options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + --lockfile-only Only use the yarn.lock file for dependency information. + No network calls will be made. --production,--prod Exclude development dependencies. (default: true if the NODE_ENV environment variable is set to "production", otherwise false) --gather-license-texts Search for license files in components and include them as license evidence. diff --git a/src/builders.ts b/src/builders.ts index 9be4af4d..5374ab40 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -53,6 +53,7 @@ interface BomBuilderOptions { reproducible?: BomBuilder['reproducible'] shortPURLs?: BomBuilder['shortPURLs'] gatherLicenseTexts?: BomBuilder['gatherLicenseTexts'] + lockfileOnly?: BomBuilder['lockfileOnly'] } export class BomBuilder { @@ -65,6 +66,7 @@ export class BomBuilder { readonly reproducible: boolean readonly shortPURLs: boolean readonly gatherLicenseTexts: boolean + readonly lockfileOnly: boolean readonly console: Console @@ -84,6 +86,7 @@ export class BomBuilder { this.reproducible = options.reproducible ?? false this.shortPURLs = options.shortPURLs ?? false this.gatherLicenseTexts = options.gatherLicenseTexts ?? false + this.lockfileOnly = options.lockfileOnly ?? false this.console = console_ } @@ -157,6 +160,16 @@ export class BomBuilder { } private async makeManifestFetcher (project: Project): Promise { + if (this.lockfileOnly) { + /* eslint-disable-next-line @typescript-eslint/require-await -- needed for signature */ + return async function (pkg: Package): Promise> { + return { + name: pkg.name, + version: pkg.version + } + } + } + const fetcher = project.configuration.makeFetcher() const fetcherOptions: FetchOptions = { project, @@ -180,6 +193,12 @@ export class BomBuilder { } private async makeLicenseEvidenceFetcher (project: Project): Promise { + if (this.lockfileOnly) { + return async function * (_pkg: Package): AsyncGenerator { + // no-op, yield nothing + } + } + const fetcher = project.configuration.makeFetcher() const fetcherOptions: FetchOptions = { project, diff --git a/src/commands.ts b/src/commands.ts index 9cab8c94..4bdeac23 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -25,7 +25,7 @@ import type { Types as SerializeTypes } from '@cyclonedx/cyclonedx-library/Seria import { JSON as SerializeJSON, JsonSerializer, XML as SerializeXML, XmlSerializer } from '@cyclonedx/cyclonedx-library/Serialize' import { SpecVersionDict, Version as SpecVersion } from '@cyclonedx/cyclonedx-library/Spec' import type { CommandContext } from '@yarnpkg/core' -import { Configuration, Project, YarnVersion } from '@yarnpkg/core' +import { Configuration, Project, ThrowReport, YarnVersion } from '@yarnpkg/core' import { npath, xfs } from '@yarnpkg/fslib' import { Command, Option } from 'clipanion' import spdxExpressionParse from "spdx-expression-parse" @@ -76,6 +76,11 @@ export class MakeSbomCommand extends Command { details: 'Recursively scan workspace dependencies and emits them as Software-Bill-of-Materials(SBOM) in CycloneDX format.' }) + readonly lockfileOnly = Option.Boolean('--lockfile-only', false, { + description: 'Only use the yarn.lock file for dependency information.\n'+ + 'No network calls will be made.' + }) + /* mimic option from yarn. - see https://classic.yarnpkg.com/lang/en/docs/cli/install/#toc-yarn-install-production-true-false - see https://yarnpkg.com/cli/workspaces/focus @@ -158,6 +163,7 @@ export class MakeSbomCommand extends Command { outputReproducible: this.outputReproducible, gatherLicenseTexts: this.gatherLicenseTexts, verbosity: this.verbosity, + lockfileOnly: this.lockfileOnly, projectDir }) @@ -170,7 +176,13 @@ export class MakeSbomCommand extends Command { } myConsole.debug('DEBUG | project:', project.cwd) myConsole.debug('DEBUG | workspace:', workspace.cwd) - await workspace.project.restoreInstallState() + + if (this.lockfileOnly) { + myConsole.info('INFO | skipping workspace installation state restoration (--lockfile-only)') + await workspace.project.resolveEverything({ lockfileOnly: true, report: new ThrowReport() }) + } else { + await workspace.project.restoreInstallState() + } const extRefFactory = new FromNodePackageJsonFactories.ExternalReferenceFactory() @@ -187,7 +199,8 @@ export class MakeSbomCommand extends Command { metaComponentType: this.mcType, reproducible: this.outputReproducible, shortPURLs: this.shortPURLs, - gatherLicenseTexts: this.gatherLicenseTexts + gatherLicenseTexts: this.gatherLicenseTexts, + lockfileOnly: this.lockfileOnly }, myConsole )).buildFromWorkspace(workspace) diff --git a/tests/_data/snapshots/plain_lockfile-only.json.bin b/tests/_data/snapshots/plain_lockfile-only.json.bin new file mode 100644 index 00000000..5e523e1a --- /dev/null +++ b/tests/_data/snapshots/plain_lockfile-only.json.bin @@ -0,0 +1,102 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "version": 1, + "metadata": { + "tools": { + "components": [ + { + "type": "application", + "name": "yarn", + "version": "yarnVersion-testing" + }, + { + "type": "library", + "name": "cyclonedx-library", + "group": "@cyclonedx", + "version": "libVersion-testing", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "url": "https://github.com/CycloneDX/cyclonedx-javascript-library#readme", + "type": "website", + "comment": "as detected from PackageJson property \"homepage\"" + } + ] + }, + { + "type": "library", + "name": "yarn-plugin-cyclonedx", + "group": "@cyclonedx", + "version": "thisVersion-testing", + "author": "Jan Kowalleck", + "description": "Create CycloneDX Software Bill of Materials (SBOM) from yarn projects.", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "url": "https://github.com/CycloneDX/cyclonedx-node-yarn/issues", + "type": "issue-tracker", + "comment": "as detected from PackageJson property \"bugs.url\"" + }, + { + "url": "git+https://github.com/CycloneDX/cyclonedx-node-yarn.git", + "type": "vcs", + "comment": "as detected from PackageJson property \"repository.url\"" + }, + { + "url": "https://github.com/CycloneDX/cyclonedx-node-yarn#readme", + "type": "website", + "comment": "as detected from PackageJson property \"homepage\"" + } + ] + } + ] + }, + "component": { + "type": "application", + "name": "lockfile-only-testbed", + "version": "1.0.0", + "bom-ref": "lockfile-only-testbed@workspace:.", + "purl": "pkg:npm/lockfile-only-testbed@1.0.0" + }, + "properties": [ + { + "name": "cdx:reproducible", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "name": "is-positive", + "version": "3.1.0", + "bom-ref": "is-positive@npm:3.1.0", + "purl": "pkg:npm/is-positive@3.1.0" + } + ], + "dependencies": [ + { + "ref": "is-positive@npm:3.1.0" + }, + { + "ref": "lockfile-only-testbed@workspace:.", + "dependsOn": [ + "is-positive@npm:3.1.0" + ] + } + ] +} \ No newline at end of file diff --git a/tests/_data/snapshots/plain_lockfile-only.xml.bin b/tests/_data/snapshots/plain_lockfile-only.xml.bin new file mode 100644 index 00000000..4b86a55c --- /dev/null +++ b/tests/_data/snapshots/plain_lockfile-only.xml.bin @@ -0,0 +1,76 @@ + + + + + + + yarn + yarnVersion-testing + + + @cyclonedx + cyclonedx-library + libVersion-testing + + + Apache-2.0 + + + + + https://github.com/CycloneDX/cyclonedx-javascript-library#readme + as detected from PackageJson property "homepage" + + + + + Jan Kowalleck + @cyclonedx + yarn-plugin-cyclonedx + thisVersion-testing + Create CycloneDX Software Bill of Materials (SBOM) from yarn projects. + + + Apache-2.0 + + + + + https://github.com/CycloneDX/cyclonedx-node-yarn/issues + as detected from PackageJson property "bugs.url" + + + git+https://github.com/CycloneDX/cyclonedx-node-yarn.git + as detected from PackageJson property "repository.url" + + + https://github.com/CycloneDX/cyclonedx-node-yarn#readme + as detected from PackageJson property "homepage" + + + + + + + lockfile-only-testbed + 1.0.0 + pkg:npm/lockfile-only-testbed@1.0.0 + + + true + + + + + is-positive + 3.1.0 + pkg:npm/is-positive@3.1.0 + + + + + + + + + \ No newline at end of file diff --git a/tests/_data/snapshots/prod_lockfile-only.json.bin b/tests/_data/snapshots/prod_lockfile-only.json.bin new file mode 100644 index 00000000..5e523e1a --- /dev/null +++ b/tests/_data/snapshots/prod_lockfile-only.json.bin @@ -0,0 +1,102 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "version": 1, + "metadata": { + "tools": { + "components": [ + { + "type": "application", + "name": "yarn", + "version": "yarnVersion-testing" + }, + { + "type": "library", + "name": "cyclonedx-library", + "group": "@cyclonedx", + "version": "libVersion-testing", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "url": "https://github.com/CycloneDX/cyclonedx-javascript-library#readme", + "type": "website", + "comment": "as detected from PackageJson property \"homepage\"" + } + ] + }, + { + "type": "library", + "name": "yarn-plugin-cyclonedx", + "group": "@cyclonedx", + "version": "thisVersion-testing", + "author": "Jan Kowalleck", + "description": "Create CycloneDX Software Bill of Materials (SBOM) from yarn projects.", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "url": "https://github.com/CycloneDX/cyclonedx-node-yarn/issues", + "type": "issue-tracker", + "comment": "as detected from PackageJson property \"bugs.url\"" + }, + { + "url": "git+https://github.com/CycloneDX/cyclonedx-node-yarn.git", + "type": "vcs", + "comment": "as detected from PackageJson property \"repository.url\"" + }, + { + "url": "https://github.com/CycloneDX/cyclonedx-node-yarn#readme", + "type": "website", + "comment": "as detected from PackageJson property \"homepage\"" + } + ] + } + ] + }, + "component": { + "type": "application", + "name": "lockfile-only-testbed", + "version": "1.0.0", + "bom-ref": "lockfile-only-testbed@workspace:.", + "purl": "pkg:npm/lockfile-only-testbed@1.0.0" + }, + "properties": [ + { + "name": "cdx:reproducible", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "name": "is-positive", + "version": "3.1.0", + "bom-ref": "is-positive@npm:3.1.0", + "purl": "pkg:npm/is-positive@3.1.0" + } + ], + "dependencies": [ + { + "ref": "is-positive@npm:3.1.0" + }, + { + "ref": "lockfile-only-testbed@workspace:.", + "dependsOn": [ + "is-positive@npm:3.1.0" + ] + } + ] +} \ No newline at end of file diff --git a/tests/_data/snapshots/prod_lockfile-only.xml.bin b/tests/_data/snapshots/prod_lockfile-only.xml.bin new file mode 100644 index 00000000..4b86a55c --- /dev/null +++ b/tests/_data/snapshots/prod_lockfile-only.xml.bin @@ -0,0 +1,76 @@ + + + + + + + yarn + yarnVersion-testing + + + @cyclonedx + cyclonedx-library + libVersion-testing + + + Apache-2.0 + + + + + https://github.com/CycloneDX/cyclonedx-javascript-library#readme + as detected from PackageJson property "homepage" + + + + + Jan Kowalleck + @cyclonedx + yarn-plugin-cyclonedx + thisVersion-testing + Create CycloneDX Software Bill of Materials (SBOM) from yarn projects. + + + Apache-2.0 + + + + + https://github.com/CycloneDX/cyclonedx-node-yarn/issues + as detected from PackageJson property "bugs.url" + + + git+https://github.com/CycloneDX/cyclonedx-node-yarn.git + as detected from PackageJson property "repository.url" + + + https://github.com/CycloneDX/cyclonedx-node-yarn#readme + as detected from PackageJson property "homepage" + + + + + + + lockfile-only-testbed + 1.0.0 + pkg:npm/lockfile-only-testbed@1.0.0 + + + true + + + + + is-positive + 3.1.0 + pkg:npm/is-positive@3.1.0 + + + + + + + + + \ No newline at end of file diff --git a/tests/_data/snapshots/short-PURLs_lockfile-only.json.bin b/tests/_data/snapshots/short-PURLs_lockfile-only.json.bin new file mode 100644 index 00000000..5e523e1a --- /dev/null +++ b/tests/_data/snapshots/short-PURLs_lockfile-only.json.bin @@ -0,0 +1,102 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7", + "version": 1, + "metadata": { + "tools": { + "components": [ + { + "type": "application", + "name": "yarn", + "version": "yarnVersion-testing" + }, + { + "type": "library", + "name": "cyclonedx-library", + "group": "@cyclonedx", + "version": "libVersion-testing", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "url": "https://github.com/CycloneDX/cyclonedx-javascript-library#readme", + "type": "website", + "comment": "as detected from PackageJson property \"homepage\"" + } + ] + }, + { + "type": "library", + "name": "yarn-plugin-cyclonedx", + "group": "@cyclonedx", + "version": "thisVersion-testing", + "author": "Jan Kowalleck", + "description": "Create CycloneDX Software Bill of Materials (SBOM) from yarn projects.", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "externalReferences": [ + { + "url": "https://github.com/CycloneDX/cyclonedx-node-yarn/issues", + "type": "issue-tracker", + "comment": "as detected from PackageJson property \"bugs.url\"" + }, + { + "url": "git+https://github.com/CycloneDX/cyclonedx-node-yarn.git", + "type": "vcs", + "comment": "as detected from PackageJson property \"repository.url\"" + }, + { + "url": "https://github.com/CycloneDX/cyclonedx-node-yarn#readme", + "type": "website", + "comment": "as detected from PackageJson property \"homepage\"" + } + ] + } + ] + }, + "component": { + "type": "application", + "name": "lockfile-only-testbed", + "version": "1.0.0", + "bom-ref": "lockfile-only-testbed@workspace:.", + "purl": "pkg:npm/lockfile-only-testbed@1.0.0" + }, + "properties": [ + { + "name": "cdx:reproducible", + "value": "true" + } + ] + }, + "components": [ + { + "type": "library", + "name": "is-positive", + "version": "3.1.0", + "bom-ref": "is-positive@npm:3.1.0", + "purl": "pkg:npm/is-positive@3.1.0" + } + ], + "dependencies": [ + { + "ref": "is-positive@npm:3.1.0" + }, + { + "ref": "lockfile-only-testbed@workspace:.", + "dependsOn": [ + "is-positive@npm:3.1.0" + ] + } + ] +} \ No newline at end of file diff --git a/tests/_data/snapshots/short-PURLs_lockfile-only.xml.bin b/tests/_data/snapshots/short-PURLs_lockfile-only.xml.bin new file mode 100644 index 00000000..4b86a55c --- /dev/null +++ b/tests/_data/snapshots/short-PURLs_lockfile-only.xml.bin @@ -0,0 +1,76 @@ + + + + + + + yarn + yarnVersion-testing + + + @cyclonedx + cyclonedx-library + libVersion-testing + + + Apache-2.0 + + + + + https://github.com/CycloneDX/cyclonedx-javascript-library#readme + as detected from PackageJson property "homepage" + + + + + Jan Kowalleck + @cyclonedx + yarn-plugin-cyclonedx + thisVersion-testing + Create CycloneDX Software Bill of Materials (SBOM) from yarn projects. + + + Apache-2.0 + + + + + https://github.com/CycloneDX/cyclonedx-node-yarn/issues + as detected from PackageJson property "bugs.url" + + + git+https://github.com/CycloneDX/cyclonedx-node-yarn.git + as detected from PackageJson property "repository.url" + + + https://github.com/CycloneDX/cyclonedx-node-yarn#readme + as detected from PackageJson property "homepage" + + + + + + + lockfile-only-testbed + 1.0.0 + pkg:npm/lockfile-only-testbed@1.0.0 + + + true + + + + + is-positive + 3.1.0 + pkg:npm/is-positive@3.1.0 + + + + + + + + + \ No newline at end of file diff --git a/tests/_data/testbeds/lockfile-only/.yarnrc.yml b/tests/_data/testbeds/lockfile-only/.yarnrc.yml new file mode 100644 index 00000000..5b3bea84 --- /dev/null +++ b/tests/_data/testbeds/lockfile-only/.yarnrc.yml @@ -0,0 +1,2 @@ +enableNetwork: false +globalFolder: './cache' \ No newline at end of file diff --git a/tests/_data/testbeds/lockfile-only/README.md b/tests/_data/testbeds/lockfile-only/README.md new file mode 100644 index 00000000..aed4ea69 --- /dev/null +++ b/tests/_data/testbeds/lockfile-only/README.md @@ -0,0 +1,7 @@ +# Integration test: lockfile only + +*ATTENTION*: this demo might use known vulnerable dependencies for showcasing purposes. + +This testbed is used to verify generation of SBOMs purely relying on the locally available information in the +`yarn.lock` file. To make sure no external information is used during the test the `.yarnrc.yml` prohibits online +interactions and uses a local cache folder (that doesn't exist) instead of the global cache. diff --git a/tests/_data/testbeds/lockfile-only/package.json b/tests/_data/testbeds/lockfile-only/package.json new file mode 100644 index 00000000..08b498fe --- /dev/null +++ b/tests/_data/testbeds/lockfile-only/package.json @@ -0,0 +1,8 @@ +{ + "name": "lockfile-only-testbed", + "version": "1.0.0", + "packageManager": "yarn@4.12.0", + "dependencies": { + "is-positive": "^3.1.0" + } +} diff --git a/tests/_data/testbeds/lockfile-only/yarn.lock b/tests/_data/testbeds/lockfile-only/yarn.lock new file mode 100644 index 00000000..ccab7c9c --- /dev/null +++ b/tests/_data/testbeds/lockfile-only/yarn.lock @@ -0,0 +1,21 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"is-positive@npm:^3.1.0": + version: 3.1.0 + resolution: "is-positive@npm:3.1.0" + checksum: 10c0/0eca66aeeb63a43e3868cbd0d1c2456d2187510bba84d8745d34e7bf69765004abed65f4d0083143d08ae1f40739407050910693bf23c082b1c4d0a06f7baf4b + languageName: node + linkType: hard + +"package-lock-only-testbed@workspace:.": + version: 0.0.0-use.local + resolution: "lockfile-only-testbed@workspace:." + dependencies: + is-positive: "npm:^3.1.0" + languageName: unknown + linkType: soft diff --git a/tests/integration/index.test.js b/tests/integration/index.test.js index 68da8a34..ffca638e 100644 --- a/tests/integration/index.test.js +++ b/tests/integration/index.test.js @@ -274,6 +274,20 @@ suite('integration', () => { }) }) + suite('lockfile-only', () => { + test('plain', + () => runTest('plain', 'lockfile-only', format, ['--lockfile-only']) + ).timeout(longTestTimeout) + + test('prod', + () => runTest('prod', 'lockfile-only', format, ['--lockfile-only', '--prod']) + ).timeout(longTestTimeout) + + test('short PURLs', + () => runTest('short-PURLs', 'lockfile-only', format, ['--lockfile-only', '--short-PURLs']) + ).timeout(longTestTimeout) + }) + suite('license evidence', () => { [ 'gather-licenses', // https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1385