rollup-plugin-sbom
Version:
A rollup and vite plugin to generate SBOMs for your application
209 lines (203 loc) • 6.98 kB
JavaScript
import { resolve, dirname, join } from 'node:path';
import * as CDX from '@cyclonedx/cyclonedx-library';
import { Enums, Spec } from '@cyclonedx/cyclonedx-library';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import normalizePackageData from 'normalize-package-data';
import { createRequire } from 'node:module';
async function getPackageJson(dir) {
try {
const rawPkg = await readFile(resolve(dir, "package.json"), "utf-8");
const pkg = JSON.parse(rawPkg.toString());
normalizePackageData(pkg);
return pkg;
} catch {
return null;
}
}
async function getCorrespondingPackageFromModuleId(modulePath, traversalLimit = 10) {
if (traversalLimit === 0) {
return Promise.resolve(null);
}
const folder = dirname(modulePath);
const potentialPackagePath = join(folder, "./package.json");
let pkgJson = null;
if (existsSync(potentialPackagePath)) {
pkgJson = await getPackageJson(folder);
}
if (pkgJson !== null) {
return pkgJson;
}
return await getCorrespondingPackageFromModuleId(folder, traversalLimit - 1);
}
function convertOrganizationalEntityOptionToModel(option) {
return new CDX.Models.OrganizationalEntity({
name: option.name,
url: new Set(option.url),
contact: new CDX.Models.OrganizationalContactRepository(
option.contact.map((contact) => new CDX.Models.OrganizationalContact(contact))
)
});
}
const require = createRequire(import.meta.url);
const knownTools = ["rollup-plugin-sbom", "vite", "rollup"];
function registerPackageUrlOnComponent(component, factory) {
if (component) {
component.purl = factory.makeFromComponent(component);
component.bomRef.value = component.purl?.toString();
}
}
async function registerTools(bom, builder) {
async function registerTool(packageName) {
try {
const modulePath = require.resolve(packageName);
const pkgJson = await getCorrespondingPackageFromModuleId(modulePath);
if (pkgJson) {
const tool = builder.makeTool(pkgJson);
if (tool) {
bom.metadata.tools.tools.add(tool);
}
}
} catch {
}
}
for (const pkgName of knownTools) {
await registerTool(pkgName);
}
}
const DEFAULT_OPTIONS = {
specVersion: Spec.Version.v1dot6,
rootComponentType: Enums.ComponentType.Application,
outDir: "cyclonedx",
outFilename: "bom",
outFormats: ["json", "xml"],
saveTimestamp: true,
autodetect: true,
generateSerial: false,
includeWellKnown: true,
supplier: void 0,
properties: void 0
};
const PLUGIN_ID = "rollup-plugin-sbom";
function rollupPluginSbom(userOptions) {
const options = {
...DEFAULT_OPTIONS,
...userOptions
};
const cdxExternalReferenceFactory = new CDX.Factories.FromNodePackageJson.ExternalReferenceFactory();
const cdxLicenseFactory = new CDX.Factories.LicenseFactory();
const cdxPurlFactory = new CDX.Factories.FromNodePackageJson.PackageUrlFactory("npm");
const cdxToolBuilder = new CDX.Builders.FromNodePackageJson.ToolBuilder(cdxExternalReferenceFactory);
const cdxComponentBuilder = new CDX.Builders.FromNodePackageJson.ComponentBuilder(
cdxExternalReferenceFactory,
cdxLicenseFactory
);
const jsonSerializer = new CDX.Serialize.JsonSerializer(
new CDX.Serialize.JSON.Normalize.Factory(CDX.Spec.SpecVersionDict[options.specVersion])
);
const xmlSerializer = new CDX.Serialize.XmlSerializer(
new CDX.Serialize.XML.Normalize.Factory(CDX.Spec.SpecVersionDict[options.specVersion])
);
const metadata = new CDX.Models.Metadata({
supplier: options.supplier && convertOrganizationalEntityOptionToModel(options.supplier),
properties: options.properties && new CDX.Models.PropertyRepository(
options.properties.map(({ name, value }) => new CDX.Models.Property(name, value))
)
});
const bom = new CDX.Models.Bom({
metadata
});
const registeredPackageIds = [];
return {
name: PLUGIN_ID,
async buildStart() {
if (options.autodetect) {
try {
const rootPkg = await getPackageJson(process.cwd());
if (rootPkg) {
bom.metadata.component = cdxComponentBuilder.makeComponent(
rootPkg,
options.rootComponentType
);
bom.metadata.component.version = rootPkg.version;
registerPackageUrlOnComponent(bom.metadata.component, cdxPurlFactory);
}
} catch {
this.error("could not autodetect package.json in the current working directory");
}
}
bom.metadata.lifecycles.add(CDX.Enums.LifecyclePhase.Build);
if (options.saveTimestamp) {
bom.metadata.timestamp = /* @__PURE__ */ new Date();
}
if (options.generateSerial) {
bom.serialNumber = CDX.Utils.BomUtility.randomSerialNumber();
}
await registerTools(bom, cdxToolBuilder);
},
/**
* Register only the effectively imported third party modules from `node_modules`
*/
async moduleParsed(moduleInfo) {
const nodeModuleImportedIds = moduleInfo.importedIds.filter(
(entry) => entry.includes("node_modules") && !entry.startsWith("\0")
);
const potentialComponents = await Promise.all(
nodeModuleImportedIds.map((moduleId) => {
if (!moduleId.includes("node_modules")) {
return Promise.resolve(null);
}
return getCorrespondingPackageFromModuleId(moduleId);
})
);
const pkgs = potentialComponents.filter((entry) => !!entry);
for (const pkg of pkgs) {
const pkgId = `${pkg.name}@${pkg.version}`;
if (registeredPackageIds.includes(pkgId)) {
continue;
}
const component = cdxComponentBuilder.makeComponent(pkg, CDX.Enums.ComponentType.Library);
registerPackageUrlOnComponent(component, cdxPurlFactory);
if (component) {
bom.components.add(component);
}
registeredPackageIds.push(pkgId);
}
},
/**
* Finalize the SBOM and emit files
*/
generateBundle() {
const formatMap = {
json: jsonSerializer,
xml: xmlSerializer
};
options.outFormats.forEach((format) => {
if (!formatMap[format]) {
throw new Error(`Unsupported format: ${format}`);
}
this.emitFile({
type: "asset",
fileName: join(options.outDir, `${options.outFilename}.${format}`),
needsCodeReference: false,
source: formatMap[format].serialize(bom, {
sortLists: false,
space: " "
})
});
});
if (options.includeWellKnown) {
this.emitFile({
type: "asset",
fileName: ".well-known/sbom",
needsCodeReference: false,
source: jsonSerializer.serialize(bom, {
sortLists: false,
space: " "
})
});
}
}
};
}
export { rollupPluginSbom as default };