UNPKG

rollup-plugin-sbom

Version:

A rollup and vite plugin to generate SBOMs for your application

209 lines (203 loc) 6.98 kB
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 };