@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,122 lines (1,103 loc) • 33.4 kB
JavaScript
import { Buffer } from "node:buffer";
import { spawnSync } from "node:child_process";
import {
existsSync,
lstatSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
} from "node:fs";
import { arch as _arch, platform as _platform, homedir } from "node:os";
import { basename, delimiter, dirname, join, resolve } from "node:path";
import process from "node:process";
import { PackageURL } from "packageurl-js";
import {
DEBUG_MODE,
MAX_BUFFER,
TIMEOUT_MS,
adjustLicenseInformation,
collectExecutables,
collectSharedLibs,
dirNameStr,
extractPathEnv,
findLicenseId,
getTmpDir,
isSpdxLicenseExpression,
multiChecksumFile,
safeMkdirSync,
} from "../helpers/utils.js";
const dirName = dirNameStr;
const isWin = _platform() === "win32";
let platform = _platform();
let extn = "";
let pluginsBinSuffix = "";
if (platform === "win32") {
platform = "windows";
extn = ".exe";
}
let arch = _arch();
switch (arch) {
case "x32":
arch = "386";
break;
case "x64":
arch = "amd64";
pluginsBinSuffix = `-${platform}-amd64`;
break;
case "arm64":
pluginsBinSuffix = `-${platform}-arm64`;
break;
case "ppc64":
arch = "ppc64le";
pluginsBinSuffix = "-ppc64";
break;
}
// cdxgen plugins version
const CDXGEN_PLUGINS_VERSION = "1.6.10";
// Retrieve the cdxgen plugins directory
let CDXGEN_PLUGINS_DIR = process.env.CDXGEN_PLUGINS_DIR;
let extraNMBinPath;
// Is there a non-empty local plugins directory
if (
!CDXGEN_PLUGINS_DIR &&
existsSync(join(dirName, "plugins")) &&
existsSync(join(dirName, "plugins", "trivy"))
) {
CDXGEN_PLUGINS_DIR = join(dirName, "plugins");
}
// Is there a non-empty local node_modules directory
if (
!CDXGEN_PLUGINS_DIR &&
existsSync(
join(
dirName,
"node_modules",
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
),
) &&
existsSync(
join(
dirName,
"node_modules",
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
"trivy",
),
)
) {
CDXGEN_PLUGINS_DIR = join(
dirName,
"node_modules",
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
);
if (existsSync(join(dirName, "node_modules", ".bin"))) {
extraNMBinPath = join(dirName, "node_modules", ".bin");
}
}
if (!CDXGEN_PLUGINS_DIR) {
let globalNodePath = process.env.GLOBAL_NODE_MODULES_PATH || undefined;
if (!globalNodePath) {
if (DEBUG_MODE) {
console.log(
`Trying to find the global node_modules path with "pnpm root -g" command.`,
);
}
const result = spawnSync(isWin ? "pnpm.cmd" : "pnpm", ["root", "-g"], {
encoding: "utf-8",
});
if (result) {
const stdout = result.stdout;
if (stdout) {
globalNodePath = `${Buffer.from(stdout).toString().trim()}/`;
}
}
}
let globalPlugins;
if (globalNodePath) {
globalPlugins = join(
globalNodePath,
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
);
extraNMBinPath = join(
globalNodePath,
"..",
".pnpm",
"node_modules",
".bin",
);
}
// pnpm add -g
let altGlobalPlugins;
if (dirName.includes(join("node_modules", ".pnpm", "@cyclonedx+cdxgen"))) {
const tmpA = dirName.split(join("node_modules", ".pnpm"));
altGlobalPlugins = join(
tmpA[0],
"node_modules",
".pnpm",
`@cyclonedx+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
"node_modules",
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
);
if (existsSync(join(tmpA[0], "node_modules", ".bin"))) {
extraNMBinPath = join(tmpA[0], "node_modules", ".bin");
}
} else if (dirName.includes(join(".pnpm", "@cyclonedx+cdxgen"))) {
// pnpm dlx
const tmpA = dirName.split(".pnpm");
altGlobalPlugins = join(
tmpA[0],
".pnpm",
`@cyclonedx+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
"node_modules",
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
);
if (existsSync(join(tmpA[0], ".bin"))) {
extraNMBinPath = join(tmpA[0], ".bin");
}
} else if (dirName.includes(join("caxa", "applications"))) {
// sae binaries
altGlobalPlugins = join(
dirName,
"node_modules",
"pnpm",
`@cyclonedx+cdxgen-plugins-bin${pluginsBinSuffix}@${CDXGEN_PLUGINS_VERSION}`,
"node_modules",
"@cyclonedx",
`cdxgen-plugins-bin${pluginsBinSuffix}`,
"plugins",
);
extraNMBinPath = join(dirName, "node_modules", ".bin");
}
// Set the plugins directory
if (globalPlugins && existsSync(globalPlugins)) {
CDXGEN_PLUGINS_DIR = globalPlugins;
if (DEBUG_MODE) {
console.log("Found global plugins", CDXGEN_PLUGINS_DIR);
}
} else if (altGlobalPlugins && existsSync(altGlobalPlugins)) {
CDXGEN_PLUGINS_DIR = altGlobalPlugins;
// To help detect bin commands such as atom, astgen, etc, we need to set this to the PATH variable.
if (DEBUG_MODE) {
console.log("Found global plugins", CDXGEN_PLUGINS_DIR);
}
}
}
// Set bin path for commands such as atom, astgen etc.
if (extraNMBinPath && !process.env?.PATH?.includes(extraNMBinPath)) {
process.env.PATH = `${extraNMBinPath}${delimiter}${process.env.PATH}`;
}
if (!CDXGEN_PLUGINS_DIR) {
if (DEBUG_MODE) {
console.warn(
"The optional cdxgen plugin was not found. Please install cdxgen without excluding optional dependencies if needed.",
);
}
CDXGEN_PLUGINS_DIR = "";
}
let TRIVY_BIN = process.env.TRIVY_CMD;
if (existsSync(join(CDXGEN_PLUGINS_DIR, "trivy"))) {
TRIVY_BIN = join(
CDXGEN_PLUGINS_DIR,
"trivy",
`trivy-cdxgen-${platform}-${arch}${extn}`,
);
}
let CARGO_AUDITABLE_BIN = process.env.CARGO_AUDITABLE_CMD;
if (existsSync(join(CDXGEN_PLUGINS_DIR, "cargo-auditable"))) {
CARGO_AUDITABLE_BIN = join(
CDXGEN_PLUGINS_DIR,
"cargo-auditable",
`cargo-auditable-cdxgen-${platform}-${arch}${extn}`,
);
}
let OSQUERY_BIN = process.env.OSQUERY_CMD;
if (existsSync(join(CDXGEN_PLUGINS_DIR, "osquery"))) {
OSQUERY_BIN = join(
CDXGEN_PLUGINS_DIR,
"osquery",
`osqueryi-${platform}-${arch}${extn}`,
);
// osqueryi-darwin-amd64.app/Contents/MacOS/osqueryd
if (platform === "darwin") {
OSQUERY_BIN = `${OSQUERY_BIN}.app/Contents/MacOS/osqueryd`;
}
}
let DOSAI_BIN = process.env.DOSAI_CMD;
if (existsSync(join(CDXGEN_PLUGINS_DIR, "dosai"))) {
DOSAI_BIN = join(
CDXGEN_PLUGINS_DIR,
"dosai",
`dosai-${platform}-${arch}${extn}`,
);
}
// Blint bin
const BLINT_BIN = process.env.BLINT_CMD || "blint";
// sourcekitten
let SOURCEKITTEN_BIN = process.env.SOURCEKITTEN_CMD;
if (existsSync(join(CDXGEN_PLUGINS_DIR, "sourcekitten"))) {
SOURCEKITTEN_BIN = join(CDXGEN_PLUGINS_DIR, "sourcekitten", "sourcekitten");
}
// Keep this list updated every year
const OS_DISTRO_ALIAS = {
"ubuntu-4.10": "warty",
"ubuntu-5.04": "hoary",
"ubuntu-5.10": "breezy",
"ubuntu-6.06": "dapper",
"ubuntu-6.10": "edgy",
"ubuntu-7.04": "feisty",
"ubuntu-7.10": "gutsy",
"ubuntu-8.04": "hardy",
"ubuntu-8.10": "intrepid",
"ubuntu-9.04": "jaunty",
"ubuntu-9.10": "karmic",
"ubuntu-10.04": "lucid",
"ubuntu-10.10": "maverick",
"ubuntu-11.04": "natty",
"ubuntu-11.10": "oneiric",
"ubuntu-12.04": "precise",
"ubuntu-12.10": "quantal",
"ubuntu-13.04": "raring",
"ubuntu-13.10": "saucy",
"ubuntu-14.04": "trusty",
"ubuntu-14.10": "utopic",
"ubuntu-15.04": "vivid",
"ubuntu-15.10": "wily",
"ubuntu-16.04": "xenial",
"ubuntu-16.10": "yakkety",
"ubuntu-17.04": "zesty",
"ubuntu-17.10": "artful",
"ubuntu-18.04": "bionic",
"ubuntu-18.10": "cosmic",
"ubuntu-19.04": "disco",
"ubuntu-19.10": "eoan",
"ubuntu-20.04": "focal",
"ubuntu-20.10": "groovy",
"ubuntu-21.04": "hirsute",
"ubuntu-21.10": "impish",
"ubuntu-22.04": "jammy",
"ubuntu-22.10": "kinetic",
"ubuntu-23.04": "lunar",
"ubuntu-23.10": "mantic",
"ubuntu-24.04": "noble",
"ubuntu-24.10": "oracular",
"ubuntu-25.04": "plucky",
"debian-14": "forky",
"debian-14.5": "forky",
"debian-13": "trixie",
"debian-13.5": "trixie",
"debian-12": "bookworm",
"debian-12.5": "bookworm",
"debian-12.6": "bookworm",
"debian-11": "bullseye",
"debian-11.5": "bullseye",
"debian-10": "buster",
"debian-10.5": "buster",
"debian-9": "stretch",
"debian-9.5": "stretch",
"debian-8": "jessie",
"debian-8.5": "jessie",
"debian-7": "wheezy",
"debian-7.5": "wheezy",
"debian-6": "squeeze",
"debian-5": "lenny",
"debian-4": "etch",
"debian-3.1": "sarge",
"debian-3": "woody",
"debian-2.2": "potato",
"debian-2.1": "slink",
"debian-2": "hamm",
"debian-1.3": "bo",
"debian-1.2": "rex",
"debian-1.1": "buzz",
"red hat enterprise linux": "rhel",
"red hat enterprise linux 6": "rhel-6",
"red hat enterprise linux 7": "rhel-7",
"red hat enterprise linux 8": "rhel-8",
"red hat enterprise linux 9": "rhel-9",
};
// TODO: Move the lists to a config file
const COMMON_RUNTIMES = [
"java",
"node",
"nodejs",
"nodejs-current",
"deno",
"bun",
"python",
"python3",
"ruby",
"php",
"php7",
"php8",
"perl",
"openjdk",
"openjdk8",
"openjdk11",
"openjdk17",
"openjdk21",
"openjdk8-jdk",
"openjdk11-jdk",
"openjdk17-jdk",
"openjdk21-jdk",
"openjdk8-jre",
"openjdk11-jre",
"openjdk17-jre",
"openjdk21-jre",
];
export function getCargoAuditableInfo(src) {
if (CARGO_AUDITABLE_BIN) {
const result = spawnSync(CARGO_AUDITABLE_BIN, [src], {
encoding: "utf-8",
});
if (result.status !== 0 || result.error) {
if (result.stdout || result.stderr) {
console.error(result.stdout, result.stderr);
}
}
if (result) {
const stdout = result.stdout;
if (stdout) {
return Buffer.from(stdout).toString();
}
}
}
return undefined;
}
/**
* Execute sourcekitten plugin with the given arguments
*
* @param args {Array} Arguments
* @returns {undefined|Object} Command output
*/
export function executeSourcekitten(args) {
if (SOURCEKITTEN_BIN) {
const result = spawnSync(SOURCEKITTEN_BIN, args, {
encoding: "utf-8",
maxBuffer: MAX_BUFFER,
});
if (result.status !== 0 || result.error) {
if (result.stdout || result.stderr) {
console.error(result.stdout, result.stderr);
}
}
if (result) {
const stdout = result.stdout;
if (stdout) {
return JSON.parse(Buffer.from(stdout).toString());
}
}
}
return undefined;
}
/**
* Get the packages installed in the container image filesystem.
*
* @param src {String} Source directory containing the extracted filesystem.
* @param imageConfig {Object} Image configuration containing environment variables, command, entrypoints etc
*
* @returns {Object} Metadata containing packages, dependencies, etc
*/
export async function getOSPackages(src, imageConfig) {
const pkgList = [];
const dependenciesList = [];
const allTypes = new Set();
const bundledSdks = new Set();
const bundledRuntimes = new Set();
const binPaths = extractPathEnv(imageConfig?.Env);
if (TRIVY_BIN) {
let imageType = "image";
const trivyCacheDir = join(homedir(), ".cache", "trivy");
try {
safeMkdirSync(join(trivyCacheDir, "db"), { recursive: true });
safeMkdirSync(join(trivyCacheDir, "java-db"), { recursive: true });
} catch (err) {
// ignore errors
}
if (existsSync(src)) {
imageType = "rootfs";
}
const tempDir = mkdtempSync(join(getTmpDir(), "trivy-cdxgen-"));
const bomJsonFile = join(tempDir, "trivy-bom.json");
const args = [
imageType,
"--skip-db-update",
"--skip-java-db-update",
"--offline-scan",
"--skip-files",
"**/*.jar,**/*.war,**/*.par,**/*.ear",
"--no-progress",
"--exit-code",
"0",
"--format",
"cyclonedx",
"--cache-dir",
trivyCacheDir,
"--output",
bomJsonFile,
];
if (!DEBUG_MODE) {
args.push("-q");
}
args.push(src);
if (DEBUG_MODE) {
console.log("Executing", TRIVY_BIN, args.join(" "));
}
const result = spawnSync(TRIVY_BIN, args, {
encoding: "utf-8",
});
if (result.status !== 0 || result.error) {
if (result.stdout || result.stderr) {
console.error(result.stdout, result.stderr);
}
}
if (existsSync(bomJsonFile)) {
let tmpBom = {};
try {
tmpBom = JSON.parse(
readFileSync(bomJsonFile, {
encoding: "utf-8",
}),
);
} catch (e) {
// ignore errors
}
// Clean up
if (tempDir?.startsWith(getTmpDir())) {
if (DEBUG_MODE) {
console.log(`Cleaning up ${tempDir}`);
}
if (rmSync) {
rmSync(tempDir, { recursive: true, force: true });
}
}
const osReleaseData = {};
let osReleaseFile = undefined;
// Let's try to read the os-release file from various locations
if (existsSync(join(src, "etc", "os-release"))) {
osReleaseFile = join(src, "etc", "os-release");
} else if (existsSync(join(src, "usr", "lib", "os-release"))) {
osReleaseFile = join(src, "usr", "lib", "os-release");
}
if (osReleaseFile) {
const osReleaseInfo = readFileSync(osReleaseFile, "utf-8");
if (osReleaseInfo) {
osReleaseInfo.split("\n").forEach((l) => {
if (!l.startsWith("#") && l.includes("=")) {
const tmpA = l.split("=");
osReleaseData[tmpA[0]] = tmpA[1].replace(/"/g, "");
}
});
}
}
if (DEBUG_MODE) {
console.log(osReleaseData);
}
let distro_codename =
osReleaseData["VERSION_CODENAME"] ||
osReleaseData["CENTOS_MANTISBT_PROJECT"] ||
osReleaseData["REDHAT_BUGZILLA_PRODUCT"] ||
osReleaseData["REDHAT_SUPPORT_PRODUCT"] ||
"";
distro_codename = distro_codename.toLowerCase();
if (distro_codename.includes(" ") && OS_DISTRO_ALIAS[distro_codename]) {
distro_codename = OS_DISTRO_ALIAS[distro_codename];
}
let distro_id = osReleaseData["ID"] || "";
const distro_id_like = osReleaseData["ID_LIKE"] || "";
let purl_type = "rpm";
switch (distro_id) {
case "debian":
case "ubuntu":
case "pop":
purl_type = "deb";
break;
case "sles":
case "suse":
case "opensuse":
purl_type = "rpm";
break;
case "alpine":
purl_type = "apk";
if (osReleaseData.VERSION_ID) {
const versionParts = osReleaseData["VERSION_ID"].split(".");
if (versionParts.length >= 2) {
distro_codename = `alpine-${versionParts[0]}.${versionParts[1]}`;
}
}
break;
default:
if (distro_id_like.includes("debian")) {
purl_type = "deb";
} else if (
distro_id_like.includes("rhel") ||
distro_id_like.includes("centos") ||
distro_id_like.includes("fedora")
) {
purl_type = "rpm";
}
break;
}
if (osReleaseData["VERSION_ID"]) {
distro_id = `${distro_id}-${osReleaseData["VERSION_ID"]}`;
if (OS_DISTRO_ALIAS[distro_id]) {
distro_codename = OS_DISTRO_ALIAS[distro_id];
}
}
const tmpDependencies = {};
(tmpBom.dependencies || []).forEach((d) => {
tmpDependencies[d.ref] = d.dependsOn;
});
if (tmpBom?.components) {
for (const comp of tmpBom.components) {
if (comp.purl) {
// Retain go components alone from trivy
if (
/^pkg:(npm|maven|pypi|cargo|composer|gem|nuget|pub|hackage|hex|conan|clojars|github)/.test(
comp.purl,
)
) {
continue;
}
const origBomRef = comp["bom-ref"];
// Fix the group
let group = dirname(comp.name);
const name = basename(comp.name);
let purlObj = undefined;
if (group === ".") {
group = "";
}
comp.group = group;
comp.name = name;
if (group === "") {
try {
purlObj = PackageURL.fromString(comp.purl);
if (purlObj.namespace && purlObj.namespace !== "") {
group = purlObj.namespace;
comp.group = group;
purlObj.namespace = group;
}
purlObj.qualifiers = purlObj.qualifiers || {};
if (distro_id?.length) {
purlObj.qualifiers["distro"] = distro_id;
}
if (distro_codename?.length) {
purlObj.qualifiers["distro_name"] = distro_codename;
}
// Bug fix for mageia and oracle linux
// Type is being returned as none for ubuntu as well!
if (purlObj.type === "none") {
purlObj["type"] = purl_type;
purlObj["namespace"] = "";
comp.group = "";
if (comp.purl?.includes(".mga")) {
purlObj["namespace"] = "mageia";
comp.group = "mageia";
purlObj.qualifiers["distro"] = "mageia";
distro_codename = "mga";
}
comp.purl = new PackageURL(
purlObj.type,
purlObj.namespace,
name,
purlObj.version,
purlObj.qualifiers,
purlObj.subpath,
).toString();
comp["bom-ref"] = decodeURIComponent(comp.purl);
}
if (purlObj.type !== "none") {
allTypes.add(purlObj.type);
}
// Prefix distro codename for ubuntu
if (purlObj.qualifiers?.distro) {
allTypes.add(purlObj.qualifiers.distro);
if (OS_DISTRO_ALIAS[purlObj.qualifiers.distro]) {
distro_codename =
OS_DISTRO_ALIAS[purlObj.qualifiers.distro];
} else if (group === "alpine") {
const dtmpA = purlObj.qualifiers.distro.split(".");
if (dtmpA && dtmpA.length > 2) {
distro_codename = `${dtmpA[0]}.${dtmpA[1]}`;
}
} else if (group === "photon") {
const dtmpA = purlObj.qualifiers.distro.split("-");
if (dtmpA && dtmpA.length > 1) {
distro_codename = dtmpA[0];
}
} else if (group === "redhat") {
const dtmpA = purlObj.qualifiers.distro.split(".");
if (dtmpA && dtmpA.length > 1) {
distro_codename = dtmpA[0].replace(
"redhat",
"enterprise_linux",
);
}
}
}
if (distro_codename !== "") {
allTypes.add(distro_codename);
allTypes.add(purlObj.namespace);
comp.purl = new PackageURL(
purlObj.type,
purlObj.namespace,
name,
purlObj.version,
purlObj.qualifiers,
purlObj.subpath,
).toString();
comp["bom-ref"] = decodeURIComponent(comp.purl);
}
} catch (err) {
// continue regardless of error
}
}
if (comp.purl.includes("epoch=")) {
try {
purlObj = PackageURL.fromString(comp.purl);
purlObj.qualifiers = purlObj.qualifiers || {};
if (distro_id?.length) {
purlObj.qualifiers["distro"] = distro_id;
}
if (distro_codename?.length) {
purlObj.qualifiers["distro_name"] = distro_codename;
}
allTypes.add(purlObj.namespace);
comp.purl = new PackageURL(
purlObj.type,
purlObj.namespace,
name,
purlObj.version,
purlObj.qualifiers,
purlObj.subpath,
).toString();
comp["bom-ref"] = decodeURIComponent(comp.purl);
} catch (err) {
// continue regardless of error
console.log(err);
}
}
// Fix licenses
if (
comp.licenses &&
Array.isArray(comp.licenses) &&
comp.licenses.length
) {
const newLicenses = [];
for (const aLic of comp.licenses) {
if (aLic.license.name) {
if (isSpdxLicenseExpression(aLic.license.name)) {
newLicenses.push({ expression: aLic.license.name });
} else {
const possibleId = findLicenseId(aLic.license.name);
if (possibleId !== aLic.license.name) {
newLicenses.push({ license: { id: possibleId } });
} else {
newLicenses.push({
license: { name: aLic.license.name },
});
}
}
} else if (
Object.keys(aLic).length &&
Object.keys(aLic.license).length
) {
newLicenses.push(aLic);
}
}
comp.licenses = adjustLicenseInformation(newLicenses);
}
// Fix hashes
if (
comp.hashes &&
Array.isArray(comp.hashes) &&
comp.hashes.length
) {
const hashContent = comp.hashes[0].content;
if (!hashContent || hashContent.length < 32) {
delete comp.hashes;
}
}
const compProperties = comp.properties;
let srcName = undefined;
let srcVersion = undefined;
if (compProperties && Array.isArray(compProperties)) {
for (const aprop of compProperties) {
if (aprop.name.endsWith("SrcName")) {
srcName = aprop.value;
}
if (aprop.name.endsWith("SrcVersion")) {
srcVersion = aprop.value;
}
}
}
delete comp.properties;
// Bug fix: We can get bom-ref like this: pkg:rpm/sles/libstdc%2B%2B6@14.2.0+git10526-150000.1.6.1?arch=x86_64&distro=sles-15.5
if (
comp["bom-ref"] &&
comp.purl &&
comp["bom-ref"] !== decodeURIComponent(comp.purl)
) {
comp["bom-ref"] = decodeURIComponent(comp.purl);
}
pkgList.push(comp);
detectSdksRuntimes(comp, bundledSdks, bundledRuntimes);
const compDeps = retrieveDependencies(
tmpDependencies,
origBomRef,
comp,
);
if (compDeps) {
dependenciesList.push(compDeps);
}
// If there is a source package defined include it as well
if (srcName && srcVersion && srcName !== comp.name) {
const newComp = Object.assign({}, comp);
newComp.name = srcName;
newComp.version = srcVersion;
if (purlObj) {
newComp.purl = new PackageURL(
purlObj.type,
purlObj.namespace,
srcName,
srcVersion,
purlObj.qualifiers,
purlObj.subpath,
).toString();
}
newComp["bom-ref"] = decodeURIComponent(newComp.purl);
pkgList.push(newComp);
detectSdksRuntimes(newComp, bundledSdks, bundledRuntimes);
}
}
}
}
}
}
let executables = [];
if (binPaths?.length) {
executables = await fileComponents(
src,
collectExecutables(src, binPaths),
"executable",
);
}
// Directories containing shared libraries
const defaultLibPaths = [
"/lib",
"/lib64",
"/usr/lib",
"/usr/lib64",
"/usr/local/lib64",
"/usr/local/lib",
"/lib/x86_64-linux-gnu",
"/usr/lib/x86_64-linux-gnu",
"/lib/i386-linux-gnu",
"/usr/lib/i386-linux-gnu",
"/lib/arm-linux-gnueabihf",
"/usr/lib/arm-linux-gnueabihf",
"/opt/**/lib",
"/root/**/lib",
];
const sharedLibs = await fileComponents(
src,
collectSharedLibs(
src,
defaultLibPaths,
"/etc/ld.so.conf",
"/etc/ld.so.conf.d/*.conf",
),
"shared_library",
);
return {
osPackages: pkgList,
dependenciesList,
allTypes: Array.from(allTypes).sort(),
bundledSdks: Array.from(bundledSdks).sort(),
bundledRuntimes: Array.from(bundledRuntimes).sort(),
binPaths,
executables,
sharedLibs,
};
}
// Detect common sdks and runtimes from the name
function detectSdksRuntimes(comp, bundledSdks, bundledRuntimes) {
if (!comp?.name) {
return;
}
if (/dotnet[6-9]?-sdk/.test(comp.name)) {
bundledSdks.add(comp.name);
}
if (
/dotnet[6-9]?-runtime/.test(comp.name) ||
comp.name.includes("aspnet-runtime") ||
/aspnetcore[6-9]?-runtime/.test(comp.name)
) {
bundledRuntimes.add(comp.name);
}
// TODO: Need to test this for a range of base images
if (COMMON_RUNTIMES.includes(comp.name)) {
bundledRuntimes.add(comp.name);
}
}
const retrieveDependencies = (tmpDependencies, origBomRef, comp) => {
try {
const tmpDependsOn = tmpDependencies[origBomRef] || [];
const dependsOn = new Set();
tmpDependsOn.forEach((d) => {
try {
const compPurl = PackageURL.fromString(comp.purl);
const tmpPurl = PackageURL.fromString(d.replace("none", compPurl.type));
tmpPurl.type = compPurl.type;
tmpPurl.namespace = compPurl.namespace;
tmpPurl.qualifiers = tmpPurl.qualifiers || {};
if (compPurl.qualifiers) {
if (compPurl.qualifiers.distro_name) {
tmpPurl.qualifiers.distro_name = compPurl.qualifiers.distro_name;
}
if (compPurl.qualifiers.distro) {
tmpPurl.qualifiers.distro = compPurl.qualifiers.distro;
}
}
dependsOn.add(decodeURIComponent(tmpPurl.toString()));
} catch (e) {
// ignore
}
});
return { ref: comp["bom-ref"], dependsOn: Array.from(dependsOn).sort() };
} catch (e) {
// ignore
}
return undefined;
};
export function executeOsQuery(query) {
if (OSQUERY_BIN) {
if (!query.endsWith(";")) {
query = `${query};`;
}
const args = ["--json", query];
// On darwin, we need to disable the safety check and run cdxgen with sudo
// https://github.com/osquery/osquery/issues/1382
if (platform === "darwin") {
args.push("--allow_unsafe");
args.push("--disable_logging");
args.push("--disable_events");
}
if (DEBUG_MODE) {
console.log("Executing", OSQUERY_BIN, args.join(" "));
}
const result = spawnSync(OSQUERY_BIN, args, {
encoding: "utf-8",
maxBuffer: 50 * 1024 * 1024,
timeout: 60 * 1000,
});
if (result.status !== 0 || result.error) {
if (
DEBUG_MODE &&
result.stderr &&
!result.stderr.includes("no such table")
) {
console.error(result.stdout, result.stderr);
}
}
if (result) {
const stdout = result.stdout;
if (stdout) {
const cmdOutput = Buffer.from(stdout).toString();
if (cmdOutput !== "") {
try {
return JSON.parse(cmdOutput);
} catch (err) {
// ignore
if (DEBUG_MODE) {
console.log("Unable to parse the output from query", query);
console.log(
"This could be due to the amount of data returned or the query being invalid for the given platform.",
);
}
}
}
return undefined;
}
}
}
return undefined;
}
/**
* Method to execute dosai to create slices for dotnet
*
* @param {string} src Source Path
* @param {string} slicesFile Slices file name
* @returns boolean
*/
export function getDotnetSlices(src, slicesFile) {
if (!DOSAI_BIN) {
return false;
}
const args = ["methods", "--path", src, "--o", slicesFile];
if (DEBUG_MODE) {
console.log("Executing", DOSAI_BIN, args.join(" "));
}
const result = spawnSync(DOSAI_BIN, args, {
encoding: "utf-8",
timeout: TIMEOUT_MS,
cwd: src,
});
if (
result?.stdout?.includes(
"You must install or update .NET to run this application",
) ||
result?.stderr?.includes(
"You must install or update .NET to run this application",
)
) {
console.log(
"Dotnet SDK is not installed. Please use the cdxgen dotnet container images to generate slices for this project.",
);
console.log(
"Alternatively, download the dosai self-contained binary (-full suffix) from https://github.com/owasp-dep-scan/dosai/releases and set the environment variable DOSAI_CMD with its location.",
);
}
if (result.status !== 0 || result.error) {
if (DEBUG_MODE && result.error) {
if (result.stderr) {
console.error(result.stdout, result.stderr);
} else {
console.log("Check if dosai plugin was installed successfully.");
}
}
return false;
}
return true;
}
/**
* Method to generate binary SBOM using blint
*
* @param {string} src Path to binary or its directory
* @param {string} binaryBomFile Path to binary
* @param {boolean} deepMode Deep mode flag
*
* @return {boolean} Result of the generation
*/
export function getBinaryBom(src, binaryBomFile, deepMode) {
if (!BLINT_BIN) {
return false;
}
const args = ["sbom", "-i", resolve(src), "-o", binaryBomFile];
if (deepMode) {
args.push("--deep");
}
if (DEBUG_MODE) {
console.log("Executing", BLINT_BIN, args.join(" "));
}
const cwd = lstatSync(src).isDirectory() ? src : dirname(src);
const result = spawnSync(BLINT_BIN, args, {
encoding: "utf-8",
timeout: TIMEOUT_MS,
cwd,
});
if (result.status !== 0 || result.error) {
if (result.stderr) {
console.error(result.stdout, result.stderr);
} else {
console.log(
"Install blint using 'pip install blint' or use the cdxgen container image.",
);
}
return false;
}
return true;
}
async function fileComponents(basePath, fileList, fileType) {
const components = [];
for (let f of fileList) {
let hashes;
try {
const hashValues = await multiChecksumFile(
["md5", "sha1"],
join(basePath, f),
);
hashes = [
{ alg: "MD5", content: hashValues["md5"] },
{ alg: "SHA-1", content: hashValues["sha1"] },
];
} catch (e) {
// ignore
}
// Collect methods returns relative paths from the extracted directory.
// We make them absolute by prefixing / here
if (!f.startsWith("/")) {
f = `/${f}`;
}
const name = basename(f);
const purl = `pkg:generic/${name}`;
let isExecutable;
let isSetuid;
let isSetgid;
let isSticky;
try {
const stats = statSync(f);
const mode = stats.mode;
isExecutable = !!(mode & 0o111);
isSetuid = !!(mode & 0o4000);
isSetgid = !!(mode & 0o2000);
isSticky = !!(mode & 0o1000);
} catch (e) {
// ignore
}
const properties = [{ name: "SrcFile", value: f }];
if (fileType === "executable" && isExecutable !== undefined) {
properties.push({
name: `internal:is_${fileType}`,
value: isExecutable.toString(),
});
} else {
properties.push({ name: `internal:is_${fileType}`, value: "true" });
}
if (isSetuid) {
properties.push({ name: "internal:has_setuid", value: "true" });
}
if (isSetgid) {
properties.push({ name: "internal:has_setgid", value: "true" });
}
if (isSticky) {
properties.push({ name: "internal:has_sticky", value: "true" });
}
components.push({
name,
type: "file",
purl,
"bom-ref": purl,
hashes,
properties,
evidence: {
identity: [
{
field: "purl",
confidence: 0,
methods: [
{
technique: "filename",
confidence: 0,
value: f,
},
],
concludedValue: f,
},
],
},
});
}
return components;
}