Skip to content

Commit

Permalink
feat: add support for supplier and properties, closes #12
Browse files Browse the repository at this point in the history
  • Loading branch information
janbiasi committed Apr 24, 2024
1 parent f77f94e commit 6905dd4
Show file tree
Hide file tree
Showing 8 changed files with 5,541 additions and 6 deletions.
18 changes: 18 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { readFile } from "node:fs/promises";
import { resolve, dirname, join } from "node:path";

import normalizePackageData, { type Package } from "normalize-package-data";
import { OrganizationalEntityOption } from "./types/OrganizationalEntityOption";
import * as CDX from "@cyclonedx/cyclonedx-library";

/**
* Read and normalize a `package.json` under the defined `dir`
Expand Down Expand Up @@ -49,3 +51,19 @@ export function getCorrespondingPackageFromModuleId(moduleId: string, traversalL

return getCorrespondingPackageFromModuleId(join(folder, ".."), traversalLimit - 1);
}

/**
* CycloneDX requires the use of their models and repositories, but we want to provide
* easy usage for the developers so we need to convert our simple interface to the corresponding models
* @param {OrganizationalEntityOption} option The option to convert
* @returns A CycloneDX {@link CDX.Models.OrganizationEntity}
*/
export function convertOrganizationalEntityOptionToModel(option: OrganizationalEntityOption) {
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)),
),
});
}
21 changes: 19 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import * as CDX from "@cyclonedx/cyclonedx-library";

import { type Package } from "normalize-package-data";

import { getPackageJson, getCorrespondingPackageFromModuleId } from "./helpers";
import {
getPackageJson,
getCorrespondingPackageFromModuleId,
convertOrganizationalEntityOptionToModel,
} from "./helpers";
import { registerPackageUrlOnComponent, registerTools } from "./builder";
import { DEFAULT_OPTIONS, RollupPluginSbomOptions } from "./options";

Expand Down Expand Up @@ -40,7 +44,20 @@ export default function rollupPluginSbom(userOptions?: RollupPluginSbomOptions):
new CDX.Serialize.XML.Normalize.Factory(CDX.Spec.SpecVersionDict[options.specVersion]!),
);

const bom = new CDX.Models.Bom();
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,
});

// A list of registered package identifiers (name and version) to prevent duplicates
const registeredPackageIds: string[] = [];

return {
Expand Down
26 changes: 26 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Enums, Spec } from "@cyclonedx/cyclonedx-library";
import type { OrganizationalEntityOption } from "./types/OrganizationalEntityOption";

/**
* SBOM plugin configuration options
* @see https://github.com/janbiasi/rollup-plugin-sbom?tab=readme-ov-file#configuration-options
*/
export interface RollupPluginSbomOptions {
/**
* Specification version to use, defaults to {@link Spec.Spec1dot5}
Expand Down Expand Up @@ -40,6 +45,25 @@ export interface RollupPluginSbomOptions {
* Whether to generate a SBOM in the `.well-known` directory. Defaults to `true`.
*/
includeWellKnown?: boolean;
/**
* The organization that supplied the component that the BOM describes.
* The supplier may often be the manufacturer, but may also be a distributor or repackager.
* @since 1.1.0
*/
supplier?: OrganizationalEntityOption | undefined;
/**
* Provides the ability to document properties in a name-value store.
* This provides flexibility to include data not officially supported in the standard without
* having to use additional namespaces or create extensions. Unlike key-value stores, properties
* support duplicate names, each potentially having different values.
*
* Property names of interest to the general public are encouraged to be registered in the
* CycloneDX Property Taxonomy. Formal registration is OPTIONAL.
*
* @since 1.1.0
* @see https://github.com/CycloneDX/cyclonedx-property-taxonomy
*/
properties?: { name: string; value: string }[] | undefined;
}

export const DEFAULT_OPTIONS: Required<RollupPluginSbomOptions> = {
Expand All @@ -52,4 +76,6 @@ export const DEFAULT_OPTIONS: Required<RollupPluginSbomOptions> = {
autodetect: true,
generateSerial: false,
includeWellKnown: true,
supplier: undefined,
properties: undefined,
};
41 changes: 41 additions & 0 deletions src/types/OrganizationalEntityOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Configuration schema for an organizational entity which will be converted to a CycloneDX model internally
* @see https://cyclonedx-javascript-library.readthedocs.io/en/latest/typedoc/node/classes/Models.OrganizationalEntity.html
*/
export interface OrganizationalEntityOption {
/**
* The name of the organization
* @example
* "Acme Inc."
*/
name?: string;
/**
* The URL of the organization. Multiple URLs are allowed.
* @example
* "https://example.com"
*/
url: string[];
/**
* A contact at the organization. Multiple contacts are allowed.
*/
contact: {
/**
* The name of a contact
* @example
* "Contact name"
*/
name?: string;
/**
* The email address of the contact.
* @example
* "firstname.lastname@example.com"
*/
email?: string;
/**
* The phone number of the contact.
* @example
* "800-555-1212"
*/
phone?: string;
}[];
}
22 changes: 21 additions & 1 deletion test/fixtures/vite-v5/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ export default defineConfig({
outDir: "plugin-outdir",
outFilename: "filename",
outFormats: ["json", "xml"],
saveTimestamp: true
saveTimestamp: true,
publisher: "Example Inc.",
supplier: {
name: "Supplier Example Inc",
url: ["https://example.com"],
contact: [{
name: "Contact Name",
email: "example@example.com",
phone: "111-222-4444"
}]
},
properties: [{
name: "unique-key",
value: "unique-value"
}, {
name: "duplicate-key",
value: "duplicate-value-1"
}, {
name: "duplicate-key",
value: "duplicate-value-2"
}]
})],
})
12 changes: 10 additions & 2 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function readJsonFile(path: string) {

const bomSchemaVersions = {
"v1.5": readJsonFile("./test/schemas/bom-1.5.schema.json"),
"v1.6": readJsonFile("./test/schemas/bom-1.6.schema.json"),
};

const ajv = new Ajv({
Expand Down Expand Up @@ -40,14 +41,16 @@ export function createOutputTestHelpers(fixtureName: string) {
getCompiledFileRawContent(filePath: string) {
return readFile(resolve(rootDir, "dist", filePath), "utf-8");
},
async getCompiledFileJSONContent(filePath: string): Promise<Record<string, unknown>> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async getCompiledFileJSONContent(filePath: string): Promise<Record<string, any>> {
try {
return JSON.parse(await methods.getCompiledFileRawContent(filePath));
} catch {
throw new ReferenceError(`Could not read file from ${filePath}`);
}
},
async getCompiledFileXMLContent(filePath: string): Promise<Record<string, unknown>> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async getCompiledFileXMLContent(filePath: string): Promise<Record<string, any>> {
try {
const parser = new XMLParser();
return parser.parse(await methods.getCompiledFileRawContent(filePath));
Expand All @@ -57,6 +60,11 @@ export function createOutputTestHelpers(fixtureName: string) {
},
isBomValidAccordingToSchema(version: keyof typeof bomSchemaVersions, rawFileContent: string) {
ajv.validate(bomSchemaVersions[version], JSON.parse(rawFileContent));

if (ajv.errors) {
console.error(ajv.errorsText(ajv.errors));
}

return ajv.errors ? ajv.errors.length === 0 : true;
},
};
Expand Down
Loading

0 comments on commit 6905dd4

Please sign in to comment.