@cyclonedx/webpack-plugin
Version:
Creates CycloneDX Software Bill of Materials (SBoM) from webpack projects
289 lines (284 loc) • 14.9 kB
JavaScript
"use strict";
/*!
This file is part of CycloneDX Webpack plugin.
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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CycloneDxWebpackPlugin = void 0;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const CDX = __importStar(require("@cyclonedx/cyclonedx-library"));
const normalize_package_data_1 = __importDefault(require("normalize-package-data"));
const webpack_1 = require("webpack");
const _helpers_1 = require("./_helpers");
const extractor_1 = require("./extractor");
class ValidationError extends Error {
details;
constructor(message, details) {
super(message);
this.details = details;
}
}
class CycloneDxWebpackPlugin {
specVersion;
reproducibleResults;
validateResults;
resultXml;
resultJson;
resultWellknown;
rootComponentAutodetect;
rootComponentType;
rootComponentName;
rootComponentVersion;
rootComponentBuildSystem;
rootComponentVCS;
collectEvidence;
constructor({ specVersion = CDX.Spec.Version.v1dot6, reproducibleResults = false, validateResults = true, outputLocation = './cyclonedx', includeWellknown = true, wellknownLocation = './.well-known', rootComponentAutodetect = true, rootComponentType = CDX.Enums.ComponentType.Application, rootComponentName = undefined, rootComponentVersion = undefined, rootComponentBuildSystem = undefined, rootComponentVCS = undefined, collectEvidence = false } = {}) {
this.specVersion = specVersion;
this.reproducibleResults = reproducibleResults;
this.validateResults = validateResults;
this.resultXml = (0, node_path_1.join)(outputLocation, './bom.xml');
this.resultJson = (0, node_path_1.join)(outputLocation, './bom.json');
this.resultWellknown = includeWellknown
? (0, node_path_1.join)(wellknownLocation, './sbom')
: undefined;
this.rootComponentAutodetect = rootComponentAutodetect;
this.rootComponentType = rootComponentType;
this.rootComponentName = rootComponentName;
this.rootComponentVersion = rootComponentVersion;
this.rootComponentBuildSystem = rootComponentBuildSystem;
this.rootComponentVCS = rootComponentVCS;
this.collectEvidence = collectEvidence;
}
apply(compiler) {
const pluginName = this.constructor.name;
compiler.hooks.thisCompilation.tap(pluginName, this.#compilationHook.bind(this));
}
#compilationHook(compilation) {
const pluginName = this.constructor.name;
const logger = compilation.getLogger(pluginName);
const spec = CDX.Spec.SpecVersionDict[this.specVersion];
if (spec === undefined) {
logger.warn('Skip CycloneDX SBOM generation due to unknown specVersion: %j Expected one of: %j', this.specVersion, Object.keys(CDX.Spec.SpecVersionDict));
return;
}
const cdxExternalReferenceFactory = new CDX.Factories.FromNodePackageJson.ExternalReferenceFactory();
const cdxLicenseFactory = new CDX.Factories.LicenseFactory();
const cdxPurlFactory = new CDX.Factories.FromNodePackageJson.PackageUrlFactory('npm');
const cdxComponentBuilder = new CDX.Builders.FromNodePackageJson.ComponentBuilder(cdxExternalReferenceFactory, cdxLicenseFactory);
const bom = new CDX.Models.Bom();
bom.metadata.lifecycles.add(CDX.Enums.LifecyclePhase.Build);
bom.metadata.component = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'));
const serializeOptions = {
sortLists: this.reproducibleResults,
space: 2
};
const toBeSerialized = new Map();
let xmlSerializer = undefined;
try {
xmlSerializer = new CDX.Serialize.XmlSerializer(new CDX.Serialize.XML.Normalize.Factory(spec));
}
catch {
}
if (xmlSerializer !== undefined) {
const xmlValidator = this.validateResults
? new CDX.Validation.XmlValidator(spec.version)
: undefined;
toBeSerialized.set(this.resultXml, [xmlSerializer, xmlValidator]);
}
let jsonSerializer = undefined;
try {
jsonSerializer = new CDX.Serialize.JsonSerializer(new CDX.Serialize.JSON.Normalize.Factory(spec));
}
catch {
}
if (jsonSerializer !== undefined) {
const jsonValidator = this.validateResults
? new CDX.Validation.JsonStrictValidator(spec.version)
: undefined;
toBeSerialized.set(this.resultJson, [jsonSerializer, jsonValidator]);
if (this.resultWellknown !== undefined) {
toBeSerialized.set(this.resultWellknown, [jsonSerializer, jsonValidator]);
}
}
compilation.hooks.afterOptimizeTree.tap(pluginName, (_, modules) => {
const thisLogger = logger.getChildLogger('ComponentFetcher');
const extractor = new extractor_1.Extractor(compilation, cdxComponentBuilder, cdxPurlFactory, new CDX.Utils.LicenseUtility.LicenseEvidenceGatherer());
thisLogger.log('generating components...');
for (const component of extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))) {
if (bom.metadata.component !== undefined &&
bom.metadata.component.group === component.group &&
bom.metadata.component.name === component.name &&
bom.metadata.component.version === component.version) {
thisLogger.debug('update bom.metadata.component - replace', bom.metadata.component, 'with', component);
bom.metadata.component = component;
}
else {
thisLogger.debug('add to bom.components', component);
bom.components.add(component);
}
}
thisLogger.log('generated components.');
thisLogger.log('finalizing BOM...');
this.#finalizeBom(bom, cdxComponentBuilder, cdxPurlFactory, logger.getChildLogger('BomFinalizer'));
thisLogger.log('finalized BOM.');
});
compilation.hooks.processAssets.tapPromise({
name: pluginName,
stage: webpack_1.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL
}, async () => {
const compileBomAsset = async function (file, serializer, validator) {
const thisLogger = logger.getChildLogger('BomAssetCompiler');
const serialized = serializer.serialize(bom, serializeOptions);
if (undefined !== validator) {
try {
const validationErrors = await validator.validate(serialized);
if (validationErrors !== null) {
thisLogger.debug('BOM result invalid. details: ', validationErrors);
throw new ValidationError(`Failed to generate valid BOM "${file}"\n` +
'Please report the issue and provide the npm lock file of the current project to:\n' +
'https://github.com/CycloneDX/cyclonedx-webpack-plugin/issues/new?template=ValidationError-report.md&labels=ValidationError&title=%5BValidationError%5D', validationErrors);
}
}
catch (err) {
if (err instanceof CDX.Validation.MissingOptionalDependencyError) {
thisLogger.info('skipped validate BOM:', err.message);
}
else {
thisLogger.error('unexpected error');
throw err;
}
}
}
const assetAction = compilation.getAsset(file) === undefined
? 'emitAsset'
: 'updateAsset';
compilation[assetAction](file, webpack_1.sources.CompatSource.from({
source: () => serialized
}));
return true;
};
const promises = [];
toBeSerialized.forEach(([serializer, validator], file) => {
promises.push(compileBomAsset(file, serializer, validator));
});
await Promise.all(promises);
});
}
#addRootComponentExtRefs(component, logger) {
if (typeof this.rootComponentBuildSystem === 'string' &&
this.rootComponentBuildSystem.length > 0 &&
!(0, _helpers_1.iterableSome)(component.externalReferences, ref => ref.type === CDX.Enums.ExternalReferenceType.BuildSystem)) {
component.externalReferences.add(new CDX.Models.ExternalReference(this.rootComponentBuildSystem, CDX.Enums.ExternalReferenceType.BuildSystem, { comment: 'as declared via cyclonedx-webpack-plugin config "rootComponentBuildSystem"' }));
logger.debug('Added rootComponent BuildSystem URL:', this.rootComponentBuildSystem);
}
if (typeof this.rootComponentVCS === 'string' &&
this.rootComponentVCS.length > 0 &&
!(0, _helpers_1.iterableSome)(component.externalReferences, ref => ref.type === CDX.Enums.ExternalReferenceType.VCS)) {
component.externalReferences.add(new CDX.Models.ExternalReference(this.rootComponentVCS, CDX.Enums.ExternalReferenceType.VCS, { comment: 'as declared via cyclonedx-webpack-plugin config "rootComponentVCS"' }));
logger.debug('Added rootComponent VCS URL:', this.rootComponentVCS);
}
}
#makeRootComponent(path, builder, logger) {
const thisPackageJson = this.rootComponentAutodetect
? (0, _helpers_1.getPackageDescription)(path)?.packageJson
: { name: this.rootComponentName, version: this.rootComponentVersion };
if (thisPackageJson === undefined) {
return undefined;
}
(0, normalize_package_data_1.default)(thisPackageJson, w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w); });
return builder.makeComponent(thisPackageJson);
}
#finalizeBom(bom, cdxComponentBuilder, cdxPurlFactory, logger) {
bom.serialNumber = this.reproducibleResults
? undefined
: CDX.Utils.BomUtility.randomSerialNumber();
bom.metadata.timestamp = this.reproducibleResults
? undefined
: new Date();
bom.metadata.tools.components.add(new CDX.Models.Component(CDX.Enums.ComponentType.Application, 'webpack', { version: webpack_1.version }));
for (const toolC of this.#makeToolCs(cdxComponentBuilder, logger.getChildLogger('ToolMaker'))) {
bom.metadata.tools.components.add(toolC);
}
if (bom.metadata.component !== undefined) {
this.#addRootComponentExtRefs(bom.metadata.component, logger);
bom.metadata.component.type = this.rootComponentType;
bom.metadata.component.purl = cdxPurlFactory.makeFromComponent(bom.metadata.component);
bom.metadata.component.bomRef.value = bom.metadata.component.purl?.toString();
}
}
*#makeToolCs(builder, logger) {
const packageJsonPaths = [
[(0, node_path_1.resolve)(module.path, '..', 'package.json'), CDX.Enums.ComponentType.Library]
];
const libs = [
'@cyclonedx/cyclonedx-library'
].map(s => s.split('/', 2));
const nodeModulePaths = require.resolve.paths('__some_none-native_package__') ?? [];
libsLoop: for (const lib of libs) {
for (const nodeModulePath of nodeModulePaths) {
const packageJsonPath = (0, node_path_1.resolve)(nodeModulePath, ...lib, 'package.json');
if ((0, node_fs_1.existsSync)(packageJsonPath)) {
packageJsonPaths.push([packageJsonPath, CDX.Enums.ComponentType.Library]);
continue libsLoop;
}
}
}
for (const [packageJsonPath, cType] of packageJsonPaths) {
logger.log('try to build new Tool from PkgPath', packageJsonPath);
const packageJson = (0, _helpers_1.loadJsonFile)(packageJsonPath) ?? {};
(0, normalize_package_data_1.default)(packageJson, w => { logger.debug('normalizePackageJson from PkgPath', packageJsonPath, 'caused:', w); });
const tool = builder.makeComponent(packageJson, cType);
if (tool !== undefined) {
yield tool;
}
}
}
}
exports.CycloneDxWebpackPlugin = CycloneDxWebpackPlugin;