From f6cbc3eaa0eb840dd64848e3af447b5a6edaf398 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Thu, 16 Jun 2022 16:09:43 -0700 Subject: [PATCH] build(tools): software bill of materials generation 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 --- package.json | 1 + tools/generate-sbom.ts | 112 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 tools/generate-sbom.ts diff --git a/package.json b/package.json index 9e2053e0271..b7a5c066244 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tools/generate-sbom.ts b/tools/generate-sbom.ts new file mode 100644 index 00000000000..012db4dc9a5 --- /dev/null +++ b/tools/generate-sbom.ts @@ -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 { + 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);