Skip to content

Commit

Permalink
Add script to generate SBOM (#108, open-pioneer/trails-build-tools/#62)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Beckemeyer <m.beckemeyer@conterra.de>
  • Loading branch information
arnevogt and mbeckem authored Nov 18, 2024
1 parent 27fa601 commit 569464a
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 0 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2024-11-18

- Add a new script: `pnpm run generate-sbom`.
The script generates a software bill of materials (SBOM) for the project that includes all production dependencies.
For more information, see the [relevant section in the Repository Guide](./docs/RepositoryGuide.md#pnpm-run-generate-sbom).

## 2024-10-22

- Replace `peerDependencies` with normal `dependencies` due to limitations of pnpm.
Expand All @@ -14,6 +20,12 @@
- Update eslint rules after updating typescript-eslint (see `.eslintrc`)
- Hide deprecation warnings for some legacy SASS APIs used in vite (see `vite.config.ts`)

## 2024-11-18

- Add a new script: `pnpm run generate-sbom`.
The script generates a software bill of materials (SBOM) for the project that includes all production dependencies.
For more information, see the [relevant section in the Repository Guide](./docs/RepositoryGuide.md#pnpm-run-generate-sbom).

## 2024-09-27

- Migrate to [pnpm catalogs](https://pnpm.io/catalogs) for central dependencies.
Expand Down
13 changes: 13 additions & 0 deletions docs/RepositoryGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ The report is written to `dist/license-report.html`.
The source code for report generation is located in `support/create-license-report.ts`.
Configuration happens via `support/license-config.yaml`.

### `pnpm run generate-sbom`

Generate a [CycloneDX](https://github.com/CycloneDX) SBOM (Software Bill of Materials) for the project.
The generated sbom file is written to `dist/sbom.json`.
The source code for report generation is located in `support/create-cyclonedx-sbom.ts`.
Currently only JSON encoding is supported. The generated SBOM lists all components excluding devDependencies.

The script reads the project name (and, if present, the version) from the project root's `package.json` and embeds it into the SBOM.
The current git revision (commit hash of `HEAD`) is also included.

> [!IMPORTANT]
> This command depends on [Trivy](https://github.com/aquasecurity/trivy) and can only be executed if Trivy is [installed](https://aquasecurity.github.io/trivy/latest/getting-started/installation/) globally.
### `pnpm run preview`

Starts a local http server serving the contents of the `dist/www` directory.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"build": "vite build",
"build-docs": "typedoc",
"build-license-report": "tsx ./support/create-license-report.ts",
"generate-sbom": "tsx ./support/create-cyclonedx-sbom.ts",
"preview": "vite preview",
"lint": "eslint ./src ./support --ext .js,.ts,.jsx,.tsx,.mjs,.mts,.cjs,.cts",
"eslint": "pnpm run lint",
Expand Down
125 changes: 125 additions & 0 deletions support/create-cyclonedx-sbom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import { execFileSync, execSync } from "child_process";
import { mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";

/**
* Generates SBOM (software bill of materials) using {@link https://github.com/aquasecurity/trivy Trivy}.
* The output file adheres to the {@link https://github.com/CycloneDX/specification CycloneDX specification} with JSON encoding.
*
* Important:
* This script relies on Trivy and can only be executed if Trivy is installed globally.
*/

const THIS_DIR = resolve(dirname(fileURLToPath(import.meta.url)));

const PACKAGE_DIR = resolve(THIS_DIR, "..");
const PACKAGE_JSON_PATH = resolve(PACKAGE_DIR, "package.json");
const OUTPUT_JSON_PATH = resolve(PACKAGE_DIR, "dist/sbom.json");
const TEMP_DIR = resolve(PACKAGE_DIR, "node_modules/.temp");

/**
* Additional property that is used to indicate the project's git revision
* see {@link https://github.com/CycloneDX/cyclonedx-property-taxonomy}
*/
const GIT_REVISION_PROPERTY = "open-pioneer:git_revision";

function main() {
// Ensure directory exists, then write the report
mkdirSync(dirname(OUTPUT_JSON_PATH), { recursive: true });
mkdirSync(TEMP_DIR, { recursive: true });

const sbom = createSBOM(); // Invoke trivy to generate sbom.
const projectInfo = readPackageJson();
const gitRevision = getGitRevision();

// Add additional properties
enhanceSBOM(sbom, projectInfo, gitRevision);

// Save to disk
saveSBOMFile(sbom);
}

/**
* Invoke trivy to create sbom for the project
*/
function createSBOM() {
const tempSbom = resolve(TEMP_DIR, "trivy-sbom.json");
execFileSync("trivy", ["fs", "--format", "cyclonedx", "-o", tempSbom, "."], {
encoding: "utf-8"
});
const sbomJson = JSON.parse(readFileSync(tempSbom, "utf-8"));
return sbomJson;
}

/**
* Parse additional information from project's package.json
*/
function readPackageJson(): ProjectInfo {
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, "utf-8"));

if (!packageJson["name"]) {
throw new Error("required property `name` is missing");
}

return {
name: packageJson["name"],
version: packageJson["version"]
};
}

/**
* Execute git command to retrieve git revision
*/
function getGitRevision(): string {
const revision = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trimEnd(); //use trim to remove trailing \n from stdout
return revision;
}

/**
* Merge additional properties into sbom object
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function enhanceSBOM(sbom: any, projectInfo: ProjectInfo, gitRevision: string) {
const sbomProjectMetadata = sbom["metadata"]["component"];

sbomProjectMetadata["name"] = projectInfo.name;
if (projectInfo.version) {
sbomProjectMetadata["version"] = projectInfo.version;
}

const properties = (sbomProjectMetadata["properties"] ?? []) as SBOMProperty[];
properties.push({ name: GIT_REVISION_PROPERTY, value: gitRevision });
sbomProjectMetadata["properties"] = properties;
}

/**
* Write the final SBOM to the output path.
*/
function saveSBOMFile(sbom: unknown) {
const sbomJson = JSON.stringify(sbom, undefined, 4);
writeFileSync(OUTPUT_JSON_PATH, sbomJson, "utf-8");
}

interface ProjectInfo {
name: string;
version?: string;
}

/**
* represents `property` type from CycloneDX specification
* key value pair that can be used to add information
*/
interface SBOMProperty {
name: string;
value: string;
}

try {
main();
} catch (e) {
console.error("Fatal error", e);
process.exit(1);
}

0 comments on commit 569464a

Please sign in to comment.