65 lines
3.8 KiB
JavaScript
65 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash, randomUUID } from "node:crypto";
|
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
import { resolve, relative } from "node:path";
|
|
|
|
const root = resolve(import.meta.dirname, "..");
|
|
const outputFlag = process.argv.indexOf("--output");
|
|
const output = resolve(root, outputFlag >= 0 ? process.argv[outputFlag + 1] : "release-artifacts");
|
|
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
const command = (program, args) => execFileSync(program, args, { cwd: root, encoding: "utf8", windowsHide: true }).trim();
|
|
|
|
const pnpmOutput = process.env.npm_execpath
|
|
? command(process.execPath, [process.env.npm_execpath, "list", "--prod", "--json", "--depth", "Infinity"])
|
|
: command("pnpm", ["list", "--prod", "--json", "--depth", "Infinity"]);
|
|
const listed = JSON.parse(pnpmOutput);
|
|
const components = new Map();
|
|
const collect = (dependencies = {}) => {
|
|
for (const [name, dependency] of Object.entries(dependencies)) {
|
|
if (dependency.version) components.set(`${name}@${dependency.version}`, { type: "library", name, version: dependency.version, purl: `pkg:npm/${encodeURIComponent(name)}@${dependency.version}` });
|
|
collect(dependency.dependencies);
|
|
}
|
|
};
|
|
for (const project of listed) collect(project.dependencies);
|
|
const packageJson = JSON.parse(await readFile(resolve(root, "package.json"), "utf8"));
|
|
const serialNumber = `urn:uuid:${randomUUID()}`;
|
|
const sbom = {
|
|
bomFormat: "CycloneDX",
|
|
specVersion: "1.6",
|
|
serialNumber,
|
|
version: 1,
|
|
metadata: { component: { type: "application", name: packageJson.name, version: packageJson.version }, tools: { components: [{ type: "application", name: "ProjectBrain release metadata generator", version: "1.0.0" }] } },
|
|
components: [...components.values()].sort((left, right) => left.purl.localeCompare(right.purl)),
|
|
};
|
|
|
|
const artifactFiles = [];
|
|
const collectFiles = async (directory) => {
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
const path = resolve(directory, entry.name);
|
|
if (entry.isDirectory()) await collectFiles(path);
|
|
else artifactFiles.push({ name: relative(root, path).replaceAll("\\", "/"), digest: { sha256: sha256(await readFile(path)) }, bytes: (await stat(path)).size });
|
|
}
|
|
};
|
|
await collectFiles(resolve(root, "dist"));
|
|
for (const name of ["Dockerfile.frontend", "Dockerfile.backend", "Dockerfile.unraid", "pnpm-lock.yaml"]) {
|
|
const path = resolve(root, name);
|
|
try { artifactFiles.push({ name, digest: { sha256: sha256(await readFile(path)) }, bytes: (await stat(path)).size }); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
}
|
|
artifactFiles.sort((left, right) => left.name.localeCompare(right.name));
|
|
const revision = command("git", ["rev-parse", "HEAD"]);
|
|
const provenance = {
|
|
predicateType: "https://slsa.dev/provenance/v1",
|
|
subject: artifactFiles,
|
|
predicate: {
|
|
buildDefinition: { buildType: "https://projectbrain.local/build/v1", externalParameters: { node: process.version, packageManager: packageJson.packageManager, revision }, resolvedDependencies: [{ uri: "git+local:projectbrain", digest: { sha1: revision } }] },
|
|
runDetails: { builder: { id: "https://projectbrain.local/ci" }, metadata: { invocationId: process.env.GITHUB_RUN_ID ?? `local-${revision.slice(0, 12)}`, startedOn: new Date().toISOString(), sourceDirty: command("git", ["status", "--porcelain"]) !== "" } },
|
|
},
|
|
sbom: { file: "projectbrain.cdx.json", sha256: sha256(JSON.stringify(sbom)) },
|
|
};
|
|
|
|
await mkdir(output, { recursive: true });
|
|
await writeFile(resolve(output, "projectbrain.cdx.json"), `${JSON.stringify(sbom, null, 2)}\n`);
|
|
await writeFile(resolve(output, "projectbrain.provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`);
|
|
console.log(`Generated ${components.size} component SBOM and provenance for ${artifactFiles.length} artifacts in ${output}.`);
|