diff --git a/README.md b/README.md index 03e0f232..9293b7d3 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,9 @@ $ yarn CycloneDX make-sbom (choices: "application", "framework", "library", "container", "platform", "device-driver", default: "application") --reproducible Whether to go the extra mile and make the output reproducible. This might result in loss of time- and random-based values. + --verbose,-v Increase the verbosity of messages. + Use multiple times to increase the verbosity even more. + ━━━ Details ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ diff --git a/package.json b/package.json index b40bdca5..0b08b7f2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@cyclonedx/cyclonedx-library": "^6.4.0", "@yarnpkg/cli": "^4.1.0", "@yarnpkg/core": "^4.0.3", - "@yarnpkg/fslib": "^3.0.2", "clipanion": "^4.0.0-rc.3", "packageurl-js": "^1.2.1", "xmlbuilder2": "^3.1.1" diff --git a/sources/_helpers.ts b/sources/_helpers.ts new file mode 100644 index 00000000..139f267b --- /dev/null +++ b/sources/_helpers.ts @@ -0,0 +1,43 @@ +/*! +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 { readFileSync, writeSync } from 'fs' + +export function loadJsonFile (path: string): any { + return JSON.parse(readFileSync(path, 'utf8')) + // may be replaced by `require(f, { with: { type: "json" } })` + // as soon as this spec is properly implemented. + // see https://github.com/tc39/proposal-import-attributes +} + +export async function writeAllSync (fd: number, data: string): Promise { + const b = Buffer.from(data) + const l = b.byteLength + let w = 0 + while (w < l) { + try { + w += writeSync(fd, b, w) + } catch (error: any) { + if (error.code !== 'EAGAIN') { + throw error + } + await new Promise((resolve) => setTimeout(resolve, 100)) + } + } + return w +} diff --git a/sources/index.ts b/sources/index.ts index 0aa7ec3a..7a8b9533 100644 --- a/sources/index.ts +++ b/sources/index.ts @@ -26,10 +26,10 @@ import { Project, ThrowReport } from '@yarnpkg/core' -import { type PortablePath, ppath } from '@yarnpkg/fslib' import { Command, Option } from 'clipanion' -import { generateSBOM, type OutputOptions, stdOutOutput } from './sbom' +import { makeConsoleLogger } from './logger' +import { generateSBOM, type OutputOptions, OutputStdOut } from './sbom' class SBOMCommand extends BaseCommand { static override readonly paths = [ @@ -52,8 +52,8 @@ class SBOMCommand extends BaseCommand { description: 'Which output format to use.\n(choices: "JSON", "XML", default: "JSON")' }) - outputFile = Option.String('--output-file', { - description: 'Path to the output file.\nSet to "-" to write to STDOUT.\n(default: write to STDOUT)' + outputFile = Option.String('--output-file', OutputStdOut, { + description: `Path to the output file.\nSet to "${OutputStdOut}" to write to STDOUT.\n(default: write to STDOUT)` }) /* mimic option from yarn. @@ -73,7 +73,13 @@ class SBOMCommand extends BaseCommand { description: 'Whether to go the extra mile and make the output reproducible.\nThis might result in loss of time- and random-based values.' }) + verbosity = Option.Counter('--verbose,-v', 1, { + description: 'Increase the verbosity of messages.\nUse multiple times to increase the verbosity even more.' + }) + async execute (): Promise { + const myConsole = makeConsoleLogger(this.verbosity, this.context) + const configuration = await Configuration.find( this.context.cwd, this.context.plugins @@ -92,10 +98,10 @@ class SBOMCommand extends BaseCommand { await project.restoreInstallState() } - await generateSBOM(project, workspace, configuration, { + await generateSBOM(myConsole, project, workspace, configuration, { specVersion: parseSpecVersion(this.specVersion), outputFormat: parseOutputFormat(this.outputFormat), - outputFile: parseOutputFile(workspace.cwd, this.outputFile), + outputFile: this.outputFile, componentType: parseComponenttype(this.componentType), reproducible: this.reproducible }) @@ -133,17 +139,6 @@ function parseOutputFormat ( return format } -function parseOutputFile ( - cwd: PortablePath, - outputFile: string | undefined -): OutputOptions['outputFile'] { - if (outputFile === undefined || outputFile === '-') { - return stdOutOutput - } else { - return ppath.resolve(cwd, outputFile) - } -} - function parseComponenttype (componentType: string | undefined): CDX.Enums.ComponentType { if (componentType === undefined || componentType.length === 0) { return CDX.Enums.ComponentType.Application diff --git a/sources/logger.ts b/sources/logger.ts new file mode 100644 index 00000000..3e7fd076 --- /dev/null +++ b/sources/logger.ts @@ -0,0 +1,41 @@ +/*! +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 { type BaseContext } from 'clipanion' + +function noop (): void { + // do nothing +} + +export function makeConsoleLogger (level: number, context: BaseContext): Console { + // all output shall be bound to stdError - stdOut is for result output only + const myConsole = new console.Console(context.stderr, context.stderr) + + if (level < 3) { + myConsole.debug = noop + if (level < 2) { + myConsole.info = noop + if (level < 1) { + myConsole.log = noop + } + } + } + + return myConsole +} diff --git a/sources/sbom.ts b/sources/sbom.ts index bc7f6f16..616f100c 100644 --- a/sources/sbom.ts +++ b/sources/sbom.ts @@ -26,9 +26,12 @@ import { structUtils, type Workspace } from '@yarnpkg/core' -import { type PortablePath, xfs } from '@yarnpkg/fslib' +import { openSync } from 'fs' import { PackageURL } from 'packageurl-js' +import { resolve } from 'path' +import * as process from 'process' +import { writeAllSync } from './_helpers' import { type BuildtimeDependencies, type PackageInfo, @@ -43,26 +46,25 @@ const componentBuilder = new CDX.Builders.FromNodePackageJson.ComponentBuilder( licenseFactory ) -/** - * Denotes output to standard out is desired instead of writing files. - */ -export const stdOutOutput = Symbol('__cdxyp_out2stdout') +export const OutputStdOut = '-' export interface OutputOptions { specVersion: CDX.Spec.Version outputFormat: CDX.Spec.Format - /** Output file name. */ - outputFile: PortablePath | typeof stdOutOutput + outputFile: string componentType: CDX.Enums.ComponentType reproducible: boolean } export async function generateSBOM ( + myConsole: Console, project: Project, workspace: Workspace, config: Configuration, outputOptions: OutputOptions ): Promise { + myConsole.debug('DEBUG | outputOptions:', outputOptions) + const bom = new CDX.Models.Bom() await addMetadataTools(bom) @@ -116,11 +118,15 @@ export async function generateSBOM ( outputOptions.outputFormat, outputOptions.reproducible ) - if (outputOptions.outputFile === stdOutOutput) { - console.log(serializedSBoM) - } else { - await xfs.writeFilePromise(outputOptions.outputFile, serializedSBoM) - } + + myConsole.log('LOG | writing BOM to', outputOptions.outputFile) + const written = await writeAllSync( + outputOptions.outputFile === OutputStdOut + ? process.stdout.fd + : openSync(resolve(process.cwd(), outputOptions.outputFile), 'w'), + serializedSBoM + ) + myConsole.info('INFO | wrote %d bytes to %s', written, outputOptions.outputFile) } async function addMetadataTools (bom: CDX.Models.Bom): Promise { @@ -158,7 +164,9 @@ function serialize ( reproducible: OutputOptions['reproducible'] ): string { const spec = CDX.Spec.SpecVersionDict[specVersion] - if (spec === undefined) { throw new RangeError('undefined specVersion') } + if (spec === undefined) { + throw new RangeError('undefined specVersion') + } switch (outputFormat) { case CDX.Spec.Format.JSON: { const serializer = new CDX.Serialize.JsonSerializer( diff --git a/tests/integration/index.test.js b/tests/integration/index.test.js index bb200a81..dc78f4c5 100644 --- a/tests/integration/index.test.js +++ b/tests/integration/index.test.js @@ -53,6 +53,7 @@ suite('integration', () => { const makeSBOM = spawnSync( 'yarn', ['sbom', + '-vvv', '--reproducible', // no intention to test all the spec-versions nor all the output-formats - this would be not our scope. '--spec-version', latestCdxSpecVersion, diff --git a/yarn.lock b/yarn.lock index 0fa4fc47..94dc1085 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5897,7 +5897,6 @@ __metadata: "@yarnpkg/builder": "npm:4.0.0" "@yarnpkg/cli": "npm:^4.1.0" "@yarnpkg/core": "npm:^4.0.3" - "@yarnpkg/fslib": "npm:^3.0.2" c8: "npm:^9.1.0" clipanion: "npm:^4.0.0-rc.3" eslint: "npm:8.57.0"