Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

187
scripts/generate-sbom.ts Normal file
View File

@@ -0,0 +1,187 @@
#!/usr/bin/env bun
/**
* SBOM (Software Bill of Materials) generation script
*
* Generates CycloneDX 1.5 SBOMs for all (or specific) packages in the monorepo.
* SBOMs are written to each package's dist/ directory alongside the build output.
*
* Usage:
* bun scripts/generate-sbom.ts # All packages
* bun scripts/generate-sbom.ts --package @suaveplan/utils # Single package
* bun scripts/generate-sbom.ts --output-dir ./sboms # Custom output dir
*/
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { CONFIG } from "./lib/config.js";
import { discoverPackageDirs } from "./lib/discover-packages.js";
const ROOT_DIR = CONFIG.paths.root;
interface CliOptions {
specificPackage?: string;
outputDir?: string;
}
function parseArgs(): CliOptions {
const args = process.argv.slice(2);
const options: CliOptions = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--package" && i + 1 < args.length) {
options.specificPackage = args[++i];
} else if (arg.startsWith("--package=")) {
options.specificPackage = arg.slice(10);
} else if (arg === "--output-dir" && i + 1 < args.length) {
options.outputDir = args[++i];
} else if (arg.startsWith("--output-dir=")) {
options.outputDir = arg.slice(13);
}
}
return options;
}
interface PackageInfo {
name: string;
version: string;
description?: string;
license?: string;
dependencies: Record<string, string>;
dir: string;
}
async function getPackages(specificPackage?: string): Promise<PackageInfo[]> {
const pkgDirs = await discoverPackageDirs();
const packages: PackageInfo[] = [];
for (const pkgDir of pkgDirs) {
const pkgJsonPath = join(pkgDir, "package.json");
const content = await readFile(pkgJsonPath, "utf-8");
const pkg = JSON.parse(content);
if (pkg.private) continue;
if (specificPackage && pkg.name !== specificPackage) continue;
packages.push({
name: pkg.name,
version: pkg.version,
description: pkg.description,
license: pkg.license,
dependencies: pkg.dependencies ?? {},
dir: pkgDir,
});
}
return packages;
}
function generateCycloneDxSbom(pkg: PackageInfo): object {
const components = Object.entries(pkg.dependencies).map(
([name, version]) => ({
type: "library",
name,
version: resolveVersion(version),
purl: buildPurl(name, resolveVersion(version)),
})
);
return {
bomFormat: "CycloneDX",
specVersion: "1.5",
serialNumber: `urn:uuid:${crypto.randomUUID()}`,
version: 1,
metadata: {
timestamp: new Date().toISOString(),
tools: [{ name: "@suaveplan/slsa", version: "1.0.0" }],
component: {
type: "library",
name: pkg.name,
version: pkg.version,
purl: buildPurl(pkg.name, pkg.version),
description: pkg.description,
license: pkg.license,
},
},
components,
};
}
function buildPurl(name: string, version: string): string {
if (name.startsWith("@")) {
const [scope, pkg] = name.split("/");
return `pkg:npm/${encodeURIComponent(scope!)}/${pkg}@${version}`;
}
return `pkg:npm/${name}@${version}`;
}
function resolveVersion(version: string): string {
if (version.startsWith("workspace:")) {
return version.replace("workspace:", "");
}
if (version.startsWith("catalog:")) {
return version.replace("catalog:", "catalog-");
}
return version;
}
async function main() {
const options = parseArgs();
console.log("=".repeat(60));
console.log("SBOM Generation (CycloneDX 1.5)");
if (options.specificPackage) {
console.log(`Package: ${options.specificPackage}`);
}
console.log("=".repeat(60));
const packages = await getPackages(options.specificPackage);
console.log(`Found ${packages.length} packages\n`);
let generated = 0;
let failed = 0;
for (const pkg of packages) {
try {
const sbom = generateCycloneDxSbom(pkg);
const json = JSON.stringify(sbom, null, 2);
const outputDir = options.outputDir
? join(ROOT_DIR, options.outputDir)
: join(pkg.dir, "dist");
try {
await stat(outputDir);
} catch {
await mkdir(outputDir, { recursive: true });
}
const filename = "sbom.cdx.json";
const outputPath = join(outputDir, filename);
await writeFile(outputPath, json);
const depCount = Object.keys(pkg.dependencies).length;
console.log(
` ${pkg.name}@${pkg.version} - ${depCount} deps -> ${outputPath}`
);
generated++;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error(` ${pkg.name} - FAILED: ${message}`);
failed++;
}
}
console.log();
console.log("=".repeat(60));
console.log(`Generated: ${generated} | Failed: ${failed}`);
console.log("=".repeat(60));
if (failed > 0) {
process.exit(1);
}
}
main().catch(console.error);