@cyclonedx/cyclonedx-esbuild
Version:
Creates CycloneDX Software Bill of Materials (SBoM) from esbuild projects
155 lines (150 loc) • 8.12 kB
JavaScript
;
/*!
This file is part of CycloneDX generator for esbuild.
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.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.cyclonedxEsbuildPlugin = exports.PLUGIN_NAME = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const CDX = __importStar(require("@cyclonedx/cyclonedx-library"));
const _helpers_1 = require("./_helpers");
const builders_1 = require("./builders");
const logger_1 = require("./logger");
exports.PLUGIN_NAME = 'cyclonedx-esbuild';
const cyclonedxEsbuildPlugin = (opts = {}) => ({
name: exports.PLUGIN_NAME,
setup(build) {
build.initialOptions.metafile = true;
const logger = (0, logger_1.makeConsoleLogger)(process.stdout, process.stderr, LogLevelMap[build.initialOptions.logLevel ?? 'warning']);
logger.debug(`${logger_1.LogPrefixes.DEBUG} setup => opt: %j`, opts);
const options = {
gatherLicenseTexts: opts.gatherLicenseTexts ?? false,
outputReproducible: opts.outputReproducible ?? false,
specVersion: opts.specVersion ?? CDX.Spec.Version.v1dot6,
outputFile: opts.outputFile || 'bom.json',
validate: opts.validate,
mcType: opts.mcType ?? CDX.Enums.ComponentType.Application
};
logger.debug(`${logger_1.LogPrefixes.DEBUG} setup => options: %j`, options);
const serializeSpec = CDX.Spec.SpecVersionDict[options.specVersion];
if (serializeSpec === undefined) {
throw new Error(`Unknown specVersion: ${options.specVersion}`);
}
const esbuildWorkingDir = build.initialOptions.absWorkingDir || process.cwd();
build.onEnd(async (result) => {
if (result.metafile === undefined) {
throw new Error('missing result.metafile');
}
logger.info(logger_1.LogPrefixes.INFO, 'start build BOM ...');
const cdxExternalReferenceFactory = new CDX.Factories.FromNodePackageJson.ExternalReferenceFactory();
const cdxLicenseFactory = new CDX.Factories.LicenseFactory();
const cdxComponentBuilder = new CDX.Builders.FromNodePackageJson.ComponentBuilder(cdxExternalReferenceFactory, cdxLicenseFactory);
const bomBuilder = new builders_1.BomBuilder(cdxComponentBuilder, new CDX.Factories.FromNodePackageJson.PackageUrlFactory('npm'), new CDX.Utils.LicenseUtility.LicenseEvidenceGatherer());
const bom = bomBuilder.fromMetafile(result.metafile, esbuildWorkingDir, options.gatherLicenseTexts, logger);
bom.metadata.lifecycles.add(CDX.Enums.LifecyclePhase.Build);
bom.metadata.tools.components.add(new CDX.Models.Component(CDX.Enums.ComponentType.Application, 'esbuild', { version: build.esbuild.version }));
for (const toolC of (0, _helpers_1.makeToolCs)(CDX.Enums.ComponentType.Library, cdxComponentBuilder, logger)) {
bom.metadata.tools.components.add(toolC);
}
bom.serialNumber = options.outputReproducible
? undefined
: CDX.Utils.BomUtility.randomSerialNumber();
bom.metadata.timestamp = options.outputReproducible
? undefined
: new Date();
if (bom.metadata.component !== undefined) {
bom.metadata.component.type = options.mcType;
}
const serializer = new CDX.Serialize.JsonSerializer(new CDX.Serialize.JSON.Normalize.Factory(serializeSpec));
const serializeOptions = {
sortLists: options.outputReproducible,
space: 2
};
const serialized = serializer.serialize(bom, serializeOptions);
if (options.validate !== false) {
const validator = new CDX.Validation.JsonStrictValidator(serializeSpec.version);
try {
const validationErrors = await validator.validate(serialized);
if (validationErrors !== null) {
logger.debug(logger_1.LogPrefixes.DEBUG, 'BOM result invalid. details:', validationErrors);
throw new _helpers_1.ValidationError(`Failed to generate valid BOM "${options.outputFile}"\n` +
'Please report the issue and provide the npm lock file of the current project to:\n' +
'https://github.com/CycloneDX/cyclonedx-esbuild/issues/new?template=ValidationError-report.md&labels=ValidationError&title=%5BValidationError%5D', validationErrors);
}
}
catch (err) {
if (err instanceof CDX.Validation.MissingOptionalDependencyError && !options.validate) {
logger.info(logger_1.LogPrefixes.INFO, 'skipped validate BOM:', err.message);
}
else {
logger.error(logger_1.LogPrefixes.ERROR, 'unexpected error');
throw err;
}
}
}
const outputFPn = (0, node_path_1.resolve)(esbuildWorkingDir, build.initialOptions.outdir || (0, node_path_1.dirname)(build.initialOptions.outfile ?? ''), options.outputFile);
logger.debug(logger_1.LogPrefixes.DEBUG, 'outputFPn:', outputFPn);
const outputFDir = (0, node_path_1.dirname)(outputFPn);
if (!(0, node_fs_1.existsSync)(outputFDir)) {
logger.info(logger_1.LogPrefixes.INFO, 'creating directory', outputFDir);
(0, node_fs_1.mkdirSync)(outputFDir, { recursive: true });
}
logger.log(logger_1.LogPrefixes.LOG, 'writing BOM to', options.outputFile);
const written = await (0, _helpers_1.writeAllSync)((0, node_fs_1.openSync)(outputFPn, 'w'), serialized);
logger.info(logger_1.LogPrefixes.INFO, 'wrote %d bytes to %s', written, options.outputFile);
});
}
});
exports.cyclonedxEsbuildPlugin = cyclonedxEsbuildPlugin;
const LogLevelMap = {
'silent': 0,
'error': 1,
'warning': 1,
'info': 2,
'debug': 3,
'verbose': 4,
};