diff --git a/README.md b/README.md index c20a9c0bb..73fdbbbfa 100644 --- a/README.md +++ b/README.md @@ -74,46 +74,52 @@ Usage: cyclonedx-npm [options] [--] [] Create CycloneDX Software Bill of Materials (SBOM) from Node.js NPM projects. Arguments: - Path to project's manifest file. - (default: "package.json" file in current working directory) + Path to project's manifest file. + (default: "package.json" file in current working directory) Options: - --ignore-npm-errors Whether to ignore errors of NPM. - This might be used, if "npm install" was run with "--force" or "--legacy-peer-deps". - (default: false) - --package-lock-only Whether to only use the lock file, ignoring "node_modules". - This means the output will be based only on the few details in and the tree described by the "npm-shrinkwrap.json" or "package-lock.json", rather than the contents of "node_modules" directory. - (default: false) - --omit Dependency types to omit from the installation tree. - (can be set multiple times) - (choices: "dev", "optional", "peer", default: "dev" if the NODE_ENV environment variable is set to "production", otherwise empty) - --gather-license-texts Search for license files in components and include them as license evidence. - This feature is experimental. (default: false) - --flatten-components Whether to flatten the components. - This means the actual nesting of node packages is not represented in the SBOM result. - (default: false) - --short-PURLs Omit all qualifiers from PackageURLs. - This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings. - (default: false) - --spec-version Which version of CycloneDX spec to use. - (choices: "1.2", "1.3", "1.4", "1.5", "1.6", default: "1.4") - --output-reproducible Whether to go the extra mile and make the output reproducible. - This requires more resources, and might result in loss of time- and random-based-values. - (env: BOM_REPRODUCIBLE) - --output-format Which output format to use. - (choices: "JSON", "XML", default: "JSON") - --output-file Path to the output file. - Set to "-" to write to STDOUT. - (default: write to STDOUT) - --validate Validate resulting BOM before outputting. - Validation is skipped, if requirements not met. See the README. - --no-validate Disable validation of resulting BOM. - --mc-type Type of the main component. - (choices: "application", "firmware", "library", default: "application") - -v, --verbose Increase the verbosity of messages. - Use multiple times to increase the verbosity even more. - -V, --version output the version number - -h, --help display help for command + --ignore-npm-errors Whether to ignore errors of NPM. + This might be used, if "npm install" was run with "--force" or "--legacy-peer-deps". + (default: false) + --package-lock-only Whether to only use the lock file, ignoring "node_modules". + This means the output will be based only on the few details in and the tree described by the "npm-shrinkwrap.json" or "package-lock.json", rather than the contents of "node_modules" directory. + (default: false) + --omit Dependency types to omit from the installation tree. + (can be set multiple times) + (choices: "dev", "optional", "peer", default: "dev" if the NODE_ENV environment variable is set to "production", otherwise empty) + --gather-license-texts Search for license files in components and include them as license evidence. + This feature is experimental. (default: false) + --flatten-components Whether to flatten the components. + This means the actual nesting of node packages is not represented in the SBOM result. + (default: false) + --short-PURLs Omit all qualifiers from PackageURLs. + This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings. + (default: false) + --spec-version Which version of CycloneDX spec to use. + (choices: "1.2", "1.3", "1.4", "1.5", "1.6", default: "1.4") + --output-reproducible Whether to go the extra mile and make the output reproducible. + This requires more resources, and might result in loss of time- and random-based-values. + (env: BOM_REPRODUCIBLE) + --output-format Which output format to use. + (choices: "JSON", "XML", default: "JSON") + --output-file Path to the output file. + Set to "-" to write to STDOUT. + (default: write to STDOUT) + --validate Validate resulting BOM before outputting. + Validation is skipped, if requirements not met. See the README. + --no-validate Disable validation of resulting BOM. + --mc-type Type of the main component. + (choices: "application", "firmware", "library", default: "application") + -w --workspace Whether to only include dependencies for specific workspaces. + (can be set multiple times) + (default: empty) + -wr --include-workspace-root Include the workspace root when workspaces are defined using "-w" or "--workspace". + (default: false) + -no-ws --no-workspaces Do not include dependencies for workspaces. + -v, --verbose Increase the verbosity of messages. + Use multiple times to increase the verbosity even more. + -V, --version output the version number + -h, --help display help for command ``` ## Demo diff --git a/src/builders.ts b/src/builders.ts index d71b1f770..7eff80593 100644 --- a/src/builders.ts +++ b/src/builders.ts @@ -45,6 +45,9 @@ interface BomBuilderOptions { flattenComponents?: BomBuilder['flattenComponents'] shortPURLs?: BomBuilder['shortPURLs'] gatherLicenseTexts?: BomBuilder['gatherLicenseTexts'] + workspace?: BomBuilder['workspace'] + includeWorkspaceRoot?: BomBuilder['includeWorkspaceRoot'] + workspaces?: BomBuilder['workspaces'] } type cPath = string @@ -65,6 +68,9 @@ export class BomBuilder { flattenComponents: boolean shortPURLs: boolean gatherLicenseTexts: boolean + workspace: string[] + includeWorkspaceRoot: boolean + workspaces: boolean console: Console @@ -89,6 +95,9 @@ export class BomBuilder { this.flattenComponents = options.flattenComponents ?? false this.shortPURLs = options.shortPURLs ?? false this.gatherLicenseTexts = options.gatherLicenseTexts ?? false + this.workspace = options.workspace ?? [] + this.includeWorkspaceRoot = options.includeWorkspaceRoot ?? false + this.workspaces = options.workspaces ?? true this.console = console_ } @@ -175,6 +184,32 @@ export class BomBuilder { } } + for (const workspace of this.workspace) { + if (npmVersionT[0] >= 7) { + args.push(`--workspace=${workspace}`) + } else { + this.console.warn('WARN | your NPM does not support "--workspace=%s", internally skipped this option', workspace) + } + } + + // No need to set explicitly if false as this is default behaviour + if (this.includeWorkspaceRoot) { + if (npmVersionT[0] >= 8) { + args.push('--include-workspace-root=true') + } else { + this.console.warn('WARN | your NPM does not support "--include-workspace-root=true", internally skipped this option') + } + } + + // No need to set explicitly if true as this is default behaviour + if (!this.workspaces) { + if (npmVersionT[0] >= 7) { + args.push('--workspaces=false') + } else { + this.console.warn('WARN | your NPM does not support "--workspaces=false", internally skipped this option') + } + } + this.console.info('INFO | gathering dependency tree ...') this.console.debug('DEBUG | npm-ls: run npm with %j in %j', args, projectDir) let npmLsReturns: Buffer diff --git a/src/cli.ts b/src/cli.ts index d021a9984..7217037c0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -43,6 +43,9 @@ interface CommandOptions { ignoreNpmErrors: boolean packageLockOnly: boolean omit: Omittable[] + workspace: string[] + includeWorkspaceRoot: boolean + workspaces: boolean | undefined gatherLicenseTexts: boolean flattenComponents: boolean shortPURLs: boolean @@ -87,6 +90,22 @@ function makeCommand (process: NodeJS.Process): Command { : [], `"${Omittable.Dev}" if the NODE_ENV environment variable is set to "production", otherwise empty` ) + ).addOption( + new Option( + '-w, --workspace ', + 'Whether to only include dependencies for a specific workspace. ' + + '(can be set multiple times)' + ).default([], 'empty') + ).addOption( + new Option( + '-no-ws, --no-workspaces', + 'Do not include dependencies for workspaces.' + ) + ).addOption( + new Option( + '-wr, --include-workspace-root', + 'Include the workspace root when workspaces are defined using `-w` or `--workspace`.' + ).default(false) ).addOption( new Option( '--gather-license-texts', @@ -238,6 +257,20 @@ export async function run (process: NodeJS.Process): Promise { throw new Error('missing evidence') } + if (options.workspaces !== undefined && !options.workspaces) { + if (options.workspace !== undefined && options.workspace.length > 0) { + myConsole.error('ERROR | Bad config: `--workspace` option cannot be used when `--no-workspaces` is also configured') + throw new Error('bad config') + } + } + + if (options.includeWorkspaceRoot) { + if (options.workspace.length === 0) { + myConsole.error('ERROR | Bad config: `--include-workspace-root` can only be used when `--workspace` is also configured') + throw new Error('bad config') + } + } + const extRefFactory = new Factories.FromNodePackageJson.ExternalReferenceFactory() myConsole.log('LOG | gathering BOM data ...') @@ -257,7 +290,10 @@ export async function run (process: NodeJS.Process): Promise { gatherLicenseTexts: options.gatherLicenseTexts, reproducible: options.outputReproducible, flattenComponents: options.flattenComponents, - shortPURLs: options.shortPURLs + shortPURLs: options.shortPURLs, + workspace: options.workspace, + includeWorkspaceRoot: options.includeWorkspaceRoot, + workspaces: options.workspaces }, myConsole ).buildFromProjectDir(projectDir, process) diff --git a/tests/integration/cli.args-pass-through.test.js b/tests/integration/cli.args-pass-through.test.js index aae140ec8..15c529e3e 100644 --- a/tests/integration/cli.args-pass-through.test.js +++ b/tests/integration/cli.args-pass-through.test.js @@ -70,8 +70,33 @@ describe('integration.cli.args-pass-through', () => { ['package-lock-only npm 8', `8.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm8ArgsGeneral, '--package-lock-only']], ['package-lock-only npm 9', `9.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm9ArgsGeneral, '--package-lock-only']], ['package-lock-only npm 10', `10.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm10ArgsGeneral, '--package-lock-only']], - ['package-lock-only npm 11', `11.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm11ArgsGeneral, '--package-lock-only']] + ['package-lock-only npm 11', `11.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm11ArgsGeneral, '--package-lock-only']], // endregion package-lock-only + // region workspace + ['workspace not supported npm 6', `6.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm6ArgsGeneral]], + ['workspace npm 7', `7.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm7ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']], + ['workspace npm 8', `8.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm8ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']], + ['workspace npm 9', `9.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm9ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']], + ['workspace npm 10', `10.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm10ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']], + ['workspace npm 11', `11.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm11ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']], + // endregion workspace + // region include-workspace-root + ['workspace root not supported npm 6', `6.${rMinor}.${rPatch}`, ['-w', 'my-wsA', '--include-workspace-root'], [...npm6ArgsGeneral]], + ['workspace root not supported npm 7', `7.${rMinor}.${rPatch}`, ['-w', 'my-wsA', '--include-workspace-root'], [...npm7ArgsGeneral]], + ['workspace root npm 7', `7.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm7ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']], + ['workspace root npm 8', `8.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm8ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']], + ['workspace root npm 9', `9.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm9ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']], + ['workspace root npm 10', `10.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm10ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']], + ['workspace root npm 11', `11.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm11ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']], + // endregion include-workspace-root + // region workspaces + ['workspaces disabled not supported npm 6', `6.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm6ArgsGeneral]], + ['workspaces disabled npm 7', `7.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm7ArgsGeneral, '--workspaces=false']], + ['workspaces disabled npm 8', `8.${rMinor}.${rPatch}`, ['-no-ws'], [...npm8ArgsGeneral, '--workspaces=false']], + ['workspaces disabled npm 9', `9.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm9ArgsGeneral, '--workspaces=false']], + ['workspaces disabled npm 10', `10.${rMinor}.${rPatch}`, ['-no-ws'], [...npm10ArgsGeneral, '--workspaces=false']], + ['workspaces disabled npm 11', `11.${rMinor}.${rPatch}`, ['-no-ws'], [...npm11ArgsGeneral, '--workspaces=false']] + // endregion workspaces ])('%s', async (purpose, npmVersion, cdxArgs, expectedArgs) => { const logFileBase = join(tmpRootRun, purpose.replace(/\W/g, '_')) const cwd = dummyProjectsRoot