Skip to content

Commit

Permalink
build(tools): software bill of materials generation
Browse files Browse the repository at this point in the history
Added a script to generate all SBoMs.
The short hand to call the script is by running
$ yarn generate-sbom
and then it saves all the different .spdx
files under ./dist/sbom/*
where the file names are derived from
the relative path of the directory of the
build definition.

Fixes #2081

Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
petermetz committed Jun 16, 2022
1 parent a69a957 commit f6cbc3e
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"tools:validate-bundle-names": "TS_NODE_PROJECT=./tools/tsconfig.json node --trace-deprecation --experimental-modules --abort-on-uncaught-exception --loader ts-node/esm --experimental-specifier-resolution=node ./tools/validate-bundle-names.js",
"generate-api-server-config": "node ./tools/generate-api-server-config.js",
"sync-ts-config": "TS_NODE_PROJECT=tools/tsconfig.json node --experimental-json-modules --loader ts-node/esm ./tools/sync-npm-deps-to-tsc-projects.ts",
"generate-sbom": "TS_NODE_PROJECT=tools/tsconfig.json node --experimental-json-modules --loader ts-node/esm ./tools/generate-sbom.ts",
"start:api-server": "node ./packages/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js --config-file=.config.json",
"start:example-supply-chain": "yarn build:dev && cd ./examples/supply-chain-app/ && yarn --no-lockfile && yarn start",
"start:example-carbon-accounting": "CONFIG_FILE=examples/cactus-example-carbon-accounting-backend/example-config.json node examples/cactus-example-carbon-accounting-backend/dist/lib/main/typescript/carbon-accounting-app-cli.js",
Expand Down
112 changes: 112 additions & 0 deletions tools/generate-sbom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { fileURLToPath } from "url";
import { dirname } from "path";
import path from "path";
import fs from "fs-extra";
import { promisify } from "util";
import { exec, ExecOptions } from "child_process";
const execAsync = promisify(exec);

import { globby, Options as GlobbyOptions } from "globby";
import { RuntimeError } from "run-time-error";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SCRIPT_DIR = __dirname;
const PROJECT_DIR = path.join(SCRIPT_DIR, "../");
console.log(`SCRIPT_DIR=${SCRIPT_DIR}`);
console.log(`PROJECT_DIR=${PROJECT_DIR}`);

const BUILD_FILE_GLOBS = [
"**/go.mod",
"**/Cargo.toml",
"**/build.gradle*",
"yarn.lock",
];

/**
* # Software Bill of Materials Generator Script
*
* Can be used to generate all the .spdx files for the various components of
* the framework. The reason why there isn't a single SBoM file generated is
* due to the fact that different languages and their package managers are in
* use in parallel.
* For example we have
* - Rust's cargo,
* - NodeJS/Typescript: yarn/npm
* - Go modules
* - Java/Kotlin: Gradle
* - etc.
*
* Dependencies:
* To run, this script requires
* - Network connection to the internet
* - Bash shell on a Debian/Ubuntu flavored operating system
* - NodeJS (v16 or newer) installation
* - Functioning container engine (Docker/Rancher/etc.)
*
* How does it work:
* 1. It uses a list of glob patterns to find build files defining dependencies.
* For example build.gradle, yarn.lock, etc.
* 2. Once a complete list of these files have been gathered, it iterates through
* their respective directories and runs the SBoM generator tool's container
* with the current working directory set to where the build file is.
* 3. The results of each execution are saved to various .spdx files where the
* name of the file is derived from its relative path calculated from the
* project root directory. Slashes are replaced with double underscores (__).
*/
const main = async (argv: string[], env: NodeJS.ProcessEnv) => {
if (!argv) {
throw new RuntimeError(`Process argv cannot be falsy.`);
}
if (!env) {
throw new RuntimeError(`Process env cannot be falsy.`);
}

const globbyOptions: GlobbyOptions = {
cwd: PROJECT_DIR,
absolute: true,
ignore: ["**/node_modules/**"],
};
const buildFilePaths = await globby(BUILD_FILE_GLOBS, globbyOptions);
console.log(`Package paths (${buildFilePaths.length}): `, buildFilePaths);

const sbomDir = path.join(PROJECT_DIR, "dist", "sbom");
await fs.mkdirp(sbomDir);
console.log("Created SBoM dir at: ", sbomDir);

for (const buildFilePath of buildFilePaths) {
const pkgDirPath = path.dirname(buildFilePath);
const x = path.relative(PROJECT_DIR, pkgDirPath);
await generateSBoM({ dirPath: x });
}
};

export async function generateSBoM(req: { dirPath: string }): Promise<void> {
const shellCmd = `docker run --rm --volume "$(pwd)/:/repository" anchore/syft packages -o spdx /repository/`;

console.log(`SCANNING DIR: ${req.dirPath}`);

// clean up the relative file path so that instead of slashes it has underscores
// ./packages/x/build.gradle => packages_x_build_gradle
const replacePattern = new RegExp("[^\\w]", "gm");
const sbomFilename = req.dirPath.replaceAll(replacePattern, "__");
const sbomExtension = ".spdx";
const sbomName = sbomFilename.concat(sbomExtension);
console.log(`SBoM filename: ${sbomName}`);

const execOpts: ExecOptions = {
cwd: req.dirPath,
maxBuffer: 32 * 1024 * 1024, // 32 MB of stdout will be allowed
};

const { stderr, stdout } = await execAsync(shellCmd, execOpts);

console.log(`STDERR: ${stderr}`);
console.log(`STDOUT: ${stdout}`);

const sbomPath = path.join(PROJECT_DIR, "dist", "sbom", sbomName);
await fs.writeFile(sbomPath, stdout);
console.log(`Scan OK. Written SBoM to ${sbomPath}`);
}

main(process.argv, process.env);

0 comments on commit f6cbc3e

Please sign in to comment.