rollup-plugin-sbom
Version:
A rollup and vite plugin to generate SBOMs for your application
228 lines (218 loc) • 8.1 kB
JavaScript
;
const node_path = require('node:path');
const CDX = require('@cyclonedx/cyclonedx-library');
const node_fs = require('node:fs');
const promises = require('node:fs/promises');
const normalizePackageData = require('normalize-package-data');
const node_module = require('node:module');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
function _interopNamespaceCompat(e) {
if (e && typeof e === 'object' && 'default' in e) return e;
const n = Object.create(null);
if (e) {
for (const k in e) {
n[k] = e[k];
}
}
n.default = e;
return n;
}
const CDX__namespace = /*#__PURE__*/_interopNamespaceCompat(CDX);
const normalizePackageData__default = /*#__PURE__*/_interopDefaultCompat(normalizePackageData);
async function getPackageJson(dir) {
try {
const rawPkg = await promises.readFile(node_path.resolve(dir, "package.json"), "utf-8");
const pkg = JSON.parse(rawPkg.toString());
normalizePackageData__default(pkg);
return pkg;
} catch {
return null;
}
}
async function getCorrespondingPackageFromModuleId(modulePath, traversalLimit = 10) {
if (traversalLimit === 0) {
return Promise.resolve(null);
}
const folder = node_path.dirname(modulePath);
const potentialPackagePath = node_path.join(folder, "./package.json");
let pkgJson = null;
if (node_fs.existsSync(potentialPackagePath)) {
pkgJson = await getPackageJson(folder);
}
if (pkgJson !== null) {
return pkgJson;
}
return await getCorrespondingPackageFromModuleId(folder, traversalLimit - 1);
}
function convertOrganizationalEntityOptionToModel(option) {
return new CDX__namespace.Models.OrganizationalEntity({
name: option.name,
url: new Set(option.url),
contact: new CDX__namespace.Models.OrganizationalContactRepository(
option.contact.map((contact) => new CDX__namespace.Models.OrganizationalContact(contact))
)
});
}
const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
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$1.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: CDX.Spec.Version.v1dot6,
rootComponentType: CDX.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__namespace.Factories.FromNodePackageJson.ExternalReferenceFactory();
const cdxLicenseFactory = new CDX__namespace.Factories.LicenseFactory();
const cdxPurlFactory = new CDX__namespace.Factories.FromNodePackageJson.PackageUrlFactory("npm");
const cdxToolBuilder = new CDX__namespace.Builders.FromNodePackageJson.ToolBuilder(cdxExternalReferenceFactory);
const cdxComponentBuilder = new CDX__namespace.Builders.FromNodePackageJson.ComponentBuilder(
cdxExternalReferenceFactory,
cdxLicenseFactory
);
const jsonSerializer = new CDX__namespace.Serialize.JsonSerializer(
new CDX__namespace.Serialize.JSON.Normalize.Factory(CDX__namespace.Spec.SpecVersionDict[options.specVersion])
);
const xmlSerializer = new CDX__namespace.Serialize.XmlSerializer(
new CDX__namespace.Serialize.XML.Normalize.Factory(CDX__namespace.Spec.SpecVersionDict[options.specVersion])
);
const metadata = new CDX__namespace.Models.Metadata({
supplier: options.supplier && convertOrganizationalEntityOptionToModel(options.supplier),
properties: options.properties && new CDX__namespace.Models.PropertyRepository(
options.properties.map(({ name, value }) => new CDX__namespace.Models.Property(name, value))
)
});
const bom = new CDX__namespace.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__namespace.Enums.LifecyclePhase.Build);
if (options.saveTimestamp) {
bom.metadata.timestamp = /* @__PURE__ */ new Date();
}
if (options.generateSerial) {
bom.serialNumber = CDX__namespace.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__namespace.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: node_path.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: " "
})
});
}
}
};
}
module.exports = rollupPluginSbom;