UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

180 lines (170 loc) 5.19 kB
import { Buffer } from "node:buffer"; import fs from "node:fs"; import { arch } from "node:os"; import { isCycloneDxBom } from "../helpers/bomUtils.js"; import { getAllFiles, getTmpDir, isWin, safeSpawnSync, } from "../helpers/utils.js"; const ORAS_CREATED_ANNOTATION = "org.opencontainers.image.created"; function getManifestDescriptors(manifestObj) { if (Array.isArray(manifestObj?.manifests)) { return manifestObj.manifests; } if (Array.isArray(manifestObj?.referrers)) { return manifestObj.referrers; } return []; } function getRepositoryRef(image) { let repositoryRef = image; const digestIndex = repositoryRef.indexOf("@"); if (digestIndex !== -1) { repositoryRef = repositoryRef.slice(0, digestIndex); } const lastSlashIndex = repositoryRef.lastIndexOf("/"); const tagIndex = repositoryRef.lastIndexOf(":"); if (tagIndex > lastSlashIndex) { repositoryRef = repositoryRef.slice(0, tagIndex); } return repositoryRef; } function getManifestImageRef(image, manifest) { if (manifest?.reference) { return manifest.reference; } if (manifest?.digest) { return `${getRepositoryRef(image)}@${manifest.digest}`; } return undefined; } function getManifestCreatedAt(manifest) { const createdAt = manifest?.annotations?.[ORAS_CREATED_ANNOTATION]; if (!createdAt) { return undefined; } const createdAtTimestamp = Date.parse(createdAt); if (Number.isNaN(createdAtTimestamp)) { return undefined; } return createdAtTimestamp; } function selectManifestImageRef(image, manifestObj) { const manifestDescriptors = getManifestDescriptors(manifestObj); const candidates = manifestDescriptors .map((manifest, index) => { const imageRef = getManifestImageRef(image, manifest); if (!imageRef) { return undefined; } return { createdAt: getManifestCreatedAt(manifest), imageRef, index, }; }) .filter(Boolean); if (!candidates.length) { return undefined; } candidates.sort((a, b) => { if (a.createdAt !== undefined || b.createdAt !== undefined) { if (a.createdAt === undefined) { return 1; } if (b.createdAt === undefined) { return -1; } if (b.createdAt !== a.createdAt) { return b.createdAt - a.createdAt; } } return b.index - a.index; }); return candidates[0]?.imageRef; } function getBomFiles(tmpDir) { let bomFiles = getAllFiles(tmpDir, "**/*.{bom,cdx}.json"); if (!bomFiles.length) { bomFiles = getAllFiles(tmpDir, "**/bom.json"); } if (!bomFiles.length) { bomFiles = getAllFiles(tmpDir, "**/*.json"); } return bomFiles; } /** * Retrieves a CycloneDX BOM attached to an OCI image using the `oras` CLI tool. * Discovers SBOM attachments via `oras discover`, pulls the first matching * artifact, and returns the parsed BOM JSON. Retries automatically with a * platform-specific manifest when the initial platform-agnostic discovery fails. * * @param {string} image OCI image reference (e.g. `"registry.example.com/org/app:tag"`) * @param {string} [platform] OCI platform string (e.g. `"linux/amd64"`); detected automatically when omitted * @returns {Object|undefined} Parsed CycloneDX BOM JSON object, or `undefined` if not found */ export function getBomWithOras(image, platform = undefined) { const platformArch = arch() === "arm64" ? "arm64" : "amd64"; let parameters = [ "discover", "--format", "json", "--artifact-type", "sbom/cyclonedx", ]; if (platform) { parameters = parameters.concat(["--platform", platform]); } let result = safeSpawnSync("oras", parameters.concat([image]), { shell: isWin, }); if (result.status !== 0 || result.error) { if (!platform) { return getBomWithOras(image, `linux/${platformArch}`); } console.log( "Install oras by following the instructions at: https://oras.land/docs/installation", ); if (result.stderr) { console.log(result.stderr); } return undefined; } if (result.stdout) { const out = Buffer.from(result.stdout).toString(); try { const manifestObj = JSON.parse(out); const imageRef = selectManifestImageRef(image, manifestObj); if (imageRef) { const tmpDir = getTmpDir(); result = safeSpawnSync("oras", ["pull", imageRef, "-o", tmpDir], { shell: isWin, }); if (result.status !== 0 || result.error) { console.log( `Unable to pull the SBOM attachment for ${imageRef} with oras!`, ); return undefined; } const bomFiles = getBomFiles(tmpDir); for (const bomFile of bomFiles) { try { const bomJson = JSON.parse(fs.readFileSync(bomFile, "utf8")); if (isCycloneDxBom(bomJson)) { return bomJson; } } catch { // Ignore unrelated or malformed JSON files pulled alongside the SBOM. } } } else { console.log(`${image} does not contain any SBOM attachment!`); } } catch (e) { console.log(e); } } return undefined; }