@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,725 lines (1,684 loc) • 338 kB
JavaScript
import { Buffer } from "node:buffer";
import {
accessSync,
constants,
lstatSync,
readdirSync,
readFileSync,
statSync,
} from "node:fs";
import { platform as _platform, arch, homedir } from "node:os";
import { basename, dirname, join, relative, resolve, sep } from "node:path";
import process from "node:process";
import { PackageURL } from "packageurl-js";
import { gte, lte } from "semver";
import { parse } from "ssri";
import { v4 as uuidv4 } from "uuid";
import { parse as loadYaml } from "yaml";
import {
AI_INSTRUCTION_FILE_KINDS,
AI_INVENTORY_PROJECT_TYPES,
AI_SKILL_FILE_KIND,
collectAiInventory,
filterInventoryDependencies,
filterInventorySubjectsByTypes,
inventoryPropertyValue,
MCP_CONFIG_FILE_KIND,
optionIncludesAiInventoryProjectType,
summarizeAiInventory,
} from "../helpers/aiInventory.js";
import {
detectMcpInventory,
detectPythonMcpInventory,
findJSImportsExports,
} from "../helpers/analyzer.js";
import {
cleanupAsarTempDir,
extractAsarToTempDir,
parseAsarArchive,
rewriteExtractedArchivePaths,
} from "../helpers/asarutils.js";
import { expandBomAuditCategories } from "../helpers/auditCategories.js";
import {
setCycloneDxFormat,
toCycloneDxSpecVersionString,
} from "../helpers/bomUtils.js";
import { parseCaxaMetadata } from "../helpers/caxa.js";
import {
collectDosaiCryptoComponents,
collectSourceCryptoComponents,
} from "../helpers/cbomutils.js";
import {
CHROME_EXTENSION_PURL_TYPE,
collectChromeExtensionsFromPath,
collectInstalledChromeExtensions,
discoverChromiumExtensionDirs,
} from "../helpers/chromextutils.js";
import {
mergeDependencies,
mergeServices,
trimComponents,
} from "../helpers/depsUtils.js";
import {
collectDosaiServicesFromMethods,
createDosaiMethodsSlice,
isDosaiDotnetLanguage,
normalizeDosaiServiceMap,
readDosaiJsonFile,
} from "../helpers/dosai.js";
import { GIT_COMMAND } from "../helpers/envcontext.js";
import {
createHbomDocument,
ensureHbomRuntimeSupport,
ensureNoMixedHbomProjectTypes,
hasHbomProjectType,
} from "../helpers/hbom.js";
import { mergeHostInventoryBoms } from "../helpers/hostTopology.js";
import { thoughtLog } from "../helpers/logger.js";
import { enrichComponentWithMcpMetadata } from "../helpers/mcp.js";
import { isPyLockFile } from "../helpers/pylockutils.js";
import {
buildDependencyTrackBomPayload,
getDependencyTrackBomApiUrl,
} from "../helpers/remote/dependency-track.js";
import { table } from "../helpers/table.js";
import {
addEvidenceForDotnet,
addEvidenceForImports,
addPlugin,
attachIdentityTools,
buildGradleCommandArguments,
buildObjectForCocoaPod,
buildObjectForGradleModule,
CARGO_CMD,
CDXGEN_VERSION,
CLJ_CMD,
cdxgenAgent,
checksumFile,
cleanupPlugin,
collectGemModuleNames,
collectGradleDependencies,
collectJarNS,
collectMvnDependencies,
convertJarNSToPackages,
convertOSQueryResults,
createNpmWorkspacePurl,
createUVLock,
DEBUG_MODE,
DOTNET_CMD,
determineSbtVersion,
dirNameStr,
encodeForPurl,
executeParallelGradleProperties,
executePodCommand,
extractJarArchive,
extractToolRefs,
frameworksList,
generatePixiLockFile,
getAllFiles,
getCppModules,
getCratesMetadata,
getGradleCommand,
getLicenses,
getMavenCommand,
getMillCommand,
getMvnMetadata,
getNugetMetadata,
getPipFrozenTree,
getPipTreeForPackages,
getPropertyGroupTextNodes,
getPyMetadata,
getPyModules,
getSwiftPackageMetadata,
getTimestamp,
getTmpDir,
hasAnyProjectType,
includeMavenTestScope,
isAllowedHttpHost,
isDryRun,
isFeatureEnabled,
isMac,
isPackageManagerAllowed,
isPartialTree,
isSecureMode,
isValidIriReference,
LEIN_CMD,
MAX_BUFFER,
PREFER_MAVEN_DEPS_TREE,
PROJECT_TYPE_ALIASES,
parseBazelActionGraph,
parseBazelSkyframe,
parseBdistMetadata,
parseBitbucketPipelinesFile,
parseBowerJson,
parseCabalData,
parseCargoData,
parseCargoDependencyData,
parseCargoManifestDependencyData,
parseCargoTomlData,
parseCljDep,
parseCloudBuildData,
parseCmakeLikeFile,
parseCocoaDependency,
parseColliderLockData,
parseComposerJson,
parseComposerLock,
parseConanData,
parseConanLockData,
parseContainerFile,
parseContainerSpecData,
parseCsPkgData,
parseCsPkgLockData,
parseCsProjAssetsData,
parseCsProjData,
parseEdnData,
parseFlakeLock,
parseFlakeNix,
parseGemfileLockData,
parseGemspecData,
parseGitHubWorkflowData,
parseGoListDep,
parseGoModData,
parseGoModGraph,
parseGoModulesTxt,
parseGoModWhy,
parseGopkgData,
parseGosumData,
parseGradleDep,
parseGradleProperties,
parseHelmYamlData,
parseLeinDep,
parseLeiningenData,
parseMakeDFile,
parseMavenTree,
parseMillDependency,
parseMinJs,
parseMixLockData,
parseNodeShrinkwrap,
parseNupkg,
parseOpenapiSpecData,
parsePackageJsonName,
parsePaketLockData,
parsePiplockData,
parsePixiLockFile,
parsePixiTomlFile,
parsePkgJson,
parsePkgLock,
parsePnpmLock,
parsePnpmWorkspace,
parsePodfileLock,
parsePodfileTargets,
parsePom,
parsePrivadoFile,
parsePubLockData,
parsePubYamlData,
parsePyLockData,
parsePyProjectTomlFile,
parseReqFile,
parseSbtLock,
parseSbtTree,
parseSetupPyFile,
parseSwiftJsonTree,
parseSwiftResolved,
parseYarnLock,
parseYarnWorkspace,
readZipEntry,
recomputeScope,
recordActivity,
recordSensitiveFileRead,
resetActivityContext,
SWIFT_CMD,
safeExistsSync,
safeMkdirSync,
safeMkdtempSync,
safeRmSync,
safeSpawnSync,
safeUnlinkSync,
safeWriteSync,
setActivityContext,
shouldFetchLicense,
splitOutputByGradleProjects,
} from "../helpers/utils.js";
export { summarizeAiInventory } from "../helpers/aiInventory.js";
import {
cleanupTempDir,
collectInstalledExtensions,
discoverIdeExtensionDirs,
extractVsixToTempDir,
parseVsixFile,
VSCODE_EXTENSION_PURL_TYPE,
} from "../helpers/vsixutils.js";
import {
enrichOSComponentsWithTrustData,
executeOsQuery,
getBinaryBom,
getOSPackages,
getPluginToolComponents,
} from "../managers/binary.js";
import {
addSkippedSrcFiles,
exportArchive,
exportImage,
getPkgPathList,
parseImageName,
} from "../managers/docker.js";
import { DEFAULT_NPMRC_BLOCKLIST, parseNpmrc } from "../parsers/npmrc.js";
const dirName = dirNameStr;
const isWin = _platform() === "win32";
let osQueries = {};
switch (_platform()) {
case "win32":
osQueries = JSON.parse(
readFileSync(join(dirName, "data", "queries-win.json"), "utf-8"),
);
break;
case "darwin":
osQueries = JSON.parse(
readFileSync(join(dirName, "data", "queries-darwin.json"), "utf-8"),
);
break;
default:
osQueries = JSON.parse(
readFileSync(join(dirName, "data", "queries.json"), "utf-8"),
);
break;
}
const cosDbQueries = JSON.parse(
readFileSync(join(dirName, "data", "cosdb-queries.json"), "utf-8"),
);
// Construct gradle cache directory
let GRADLE_CACHE_DIR =
process.env.GRADLE_CACHE_DIR ||
join(homedir(), ".gradle", "caches", "modules-2", "files-2.1");
if (process.env.GRADLE_USER_HOME) {
GRADLE_CACHE_DIR = join(
process.env.GRADLE_USER_HOME,
"caches",
"modules-2",
"files-2.1",
);
}
// Construct path to gradle init script
const GRADLE_INIT_SCRIPT = resolve(
dirNameStr,
"data",
"helpers",
"init.gradle",
);
// Construct sbt cache directory
const SBT_CACHE_DIR =
process.env.SBT_CACHE_DIR || join(homedir(), ".ivy2", "cache");
function getCargoCacheDir() {
return process.env.CARGO_CACHE_DIR
? resolve(process.env.CARGO_CACHE_DIR)
: resolve(
process.env.CARGO_HOME || join(homedir(), ".cargo"),
"registry",
"cache",
);
}
// CycloneDX Hash pattern
const HASH_PATTERN =
"^([a-fA-F0-9]{32}|[a-fA-F0-9]{40}|[a-fA-F0-9]{64}|[a-fA-F0-9]{96}|[a-fA-F0-9]{128})$";
/**
* Creates a default parent component based on the directory name.
*
* @param {String} path Directory or file name
* @param {String} type Package type
* @param {Object} options CLI options
* @returns component object
*/
const createDefaultParentComponent = (
path,
type = "application",
options = {},
) => {
// Expands any relative path such as dot
path = resolve(path);
// Create a parent component based on the directory name
let dirNameStr =
safeExistsSync(path) && lstatSync(path).isDirectory()
? basename(path)
: dirname(path);
const tmpA = dirNameStr.split(sep);
dirNameStr = tmpA[tmpA.length - 1];
const compName = "project-name" in options ? options.projectName : dirNameStr;
const parentComponent = {
group: options.projectGroup || "",
name: compName,
version: `${options.projectVersion}` || "latest",
type:
type === "container" || compName.endsWith(".tar")
? "container"
: "application",
};
const ppurl = new PackageURL(
type,
parentComponent.group,
parentComponent.name,
parentComponent.version,
null,
null,
).toString();
parentComponent["bom-ref"] = decodeURIComponent(ppurl);
parentComponent["purl"] = ppurl;
return parentComponent;
};
const shouldIncludeNodeModulesDir = (options = {}, baseProjectTypes = []) => {
if (options.deep) {
return true;
}
const projectTypes = Array.isArray(options.projectType)
? options.projectType
: options.projectType
? [options.projectType]
: [];
if (!projectTypes.length) {
return true;
}
return baseProjectTypes.some((projectType) =>
projectTypes.every((selectedProjectType) =>
PROJECT_TYPE_ALIASES[projectType]?.includes(selectedProjectType),
),
);
};
const hasExplicitProjectTypeSelection = (options, baseProjectType) => {
options = options || {};
const projectTypes = Array.isArray(options.projectType)
? options.projectType
: options.projectType
? [options.projectType]
: [];
return projectTypes.some((selectedProjectType) =>
PROJECT_TYPE_ALIASES[baseProjectType]?.includes(selectedProjectType),
);
};
const hasDotnetProjectIndicators = (src, options = {}) => {
return Boolean(
getAllFiles(src, "**/*.{csproj,fsproj,vbproj,sln}", options)?.length,
);
};
const shouldCollectDosaiCrypto = (src, options = {}) => {
const projectTypes = Array.isArray(options.projectType)
? options.projectType
: options.projectType
? [options.projectType]
: [];
if (projectTypes.some((projectType) => isDosaiDotnetLanguage(projectType))) {
return true;
}
if (!projectTypes.length || projectTypes.includes("universal")) {
return hasDotnetProjectIndicators(src, options);
}
return false;
};
const determineParentComponent = (options) => {
let parentComponent;
if (options.parentComponent && Object.keys(options.parentComponent).length) {
return options.parentComponent;
}
if (options.projectName && options.projectVersion) {
parentComponent = {
group: options.projectGroup || "",
name: options.projectName,
version: `${options.projectVersion}` || "",
type: "application",
};
const ppurl = new PackageURL(
parentComponent.type,
parentComponent.group,
parentComponent.name,
parentComponent.version,
null,
null,
).toString();
parentComponent["bom-ref"] = decodeURIComponent(ppurl);
parentComponent["purl"] = ppurl;
}
return parentComponent;
};
const addToolsSection = (options, context = {}) => {
if (options.specVersion === 1.4) {
return [
{
vendor: "cyclonedx",
name: "cdxgen",
version: CDXGEN_VERSION,
},
];
}
let components = [];
const tools = options.tools || context.tools || [];
// tools can be an object or array
if (Array.isArray(tools) && tools.length) {
// cyclonedx-maven-plugin has the legacy tools metadata which needs to be patched
for (const tool of tools) {
if (!tool.type) {
tool.type = "application";
if (tool.vendor) {
tool.publisher = tool.vendor;
delete tool.vendor;
}
}
}
components = components.concat(tools);
} else if (tools && Object.keys(tools).length && tools.components) {
components = components.concat(tools.components);
}
const cdxToolComponent = {
group: "@cyclonedx",
name: "cdxgen",
version: CDXGEN_VERSION,
purl: `pkg:npm/%40cyclonedx/cdxgen@${CDXGEN_VERSION}`,
type: "application",
"bom-ref": `pkg:npm/@cyclonedx/cdxgen@${CDXGEN_VERSION}`,
author: "OWASP Foundation",
publisher: "OWASP Foundation",
};
if (options.specVersion >= 1.6) {
cdxToolComponent.authors = [{ name: "OWASP Foundation" }];
delete cdxToolComponent.author;
}
components.push(cdxToolComponent);
return { components };
};
const componentToSimpleFullName = (comp) => {
let fullName = comp.group?.length ? `${comp.group}/${comp.name}` : comp.name;
if (comp.version?.length) {
fullName = `${fullName}@${comp.version}`;
}
return fullName;
};
// Remove unwanted properties from parent component
// Bug #1519 - Retain licenses and external references
const cleanParentComponent = (comp) => {
delete comp.evidence;
delete comp._integrity;
if (comp.license) {
const licenses = getLicenses(comp);
if (licenses?.length) {
comp.licenses = licenses;
}
}
delete comp.license;
delete comp.qualifiers;
if (comp.repository || comp.homepage) {
const externalReferences = addExternalReferences(comp);
if (externalReferences?.length) {
comp.externalReferences = externalReferences;
}
}
delete comp.repository;
delete comp.homepage;
return comp;
};
const addAuthorsSection = (options) => {
const authors = [];
if (options.author) {
const oauthors = Array.isArray(options.author)
? options.author
: [options.author];
for (const aauthor of oauthors) {
if (aauthor.trim().length < 2) {
continue;
}
authors.push({ name: aauthor });
}
}
return authors;
};
/**
* Method to generate metadata.lifecycles section. We assume that we operate during "build"
* most of the time and under "post-build" for containers.
*
* @param {Object} options
* @returns {Array} Lifecycles array
*/
const addLifecyclesSection = (options) => {
// If lifecycle was set via CLI arguments, reuse the value
if (options.lifecycle) {
return [{ phase: options.lifecycle }];
}
const lifecycles = [{ phase: options.installDeps ? "build" : "pre-build" }];
if (options.exportData) {
const inspectData = options.exportData.inspectData;
if (inspectData) {
lifecycles.push({ phase: "post-build" });
}
} else if (
options?.projectType?.length &&
options?.projectType?.includes("binary")
) {
lifecycles.push({ phase: "post-build" });
}
if (options.projectType?.includes("os")) {
lifecycles.push({ phase: "operations" });
}
return lifecycles;
};
/**
* Function to create metadata block
*
*/
function addMetadata(parentComponent = {}, options = {}, context = {}) {
// DO NOT fork this project to just change the vendor or author's name
// Try to contribute to this project by sending PR or filing issues
const tools = addToolsSection(options, context);
const authors = addAuthorsSection(options);
const lifecycles =
options.specVersion >= 1.5 ? addLifecyclesSection(options) : undefined;
const metadata = {
timestamp: getTimestamp(),
tools,
authors,
supplier: undefined,
};
if (lifecycles) {
metadata.lifecycles = lifecycles;
}
// TLP classification
if (options.specVersion >= 1.7 && options?.tlpClassification) {
metadata.distributionConstraints = { tlp: options.tlpClassification };
}
if (parentComponent && Object.keys(parentComponent).length) {
if (parentComponent) {
cleanParentComponent(parentComponent);
if (!parentComponent["purl"] && parentComponent["bom-ref"]) {
parentComponent["purl"] = encodeForPurl(parentComponent["bom-ref"]);
}
}
if (parentComponent?.components) {
parentComponent.components = listComponents(
options,
{},
parentComponent.components,
);
const parentFullName = componentToSimpleFullName(parentComponent);
const subComponents = [];
const addedSubComponents = {};
for (const comp of parentComponent.components) {
cleanParentComponent(comp);
if (comp.name && comp.type) {
const fullName = componentToSimpleFullName(comp);
// Fixes #479
// Prevent the parent component from also appearing as a sub-component
// We cannot use purl or bom-ref here since they would not match
// purl - could have application on one side and a different type
// bom-ref could have qualifiers on one side
if (fullName !== parentFullName) {
if (!comp["bom-ref"]) {
comp["bom-ref"] = `pkg:${comp.type}/${decodeURIComponent(
fullName,
)}`;
}
if (!addedSubComponents[comp["bom-ref"]]) {
subComponents.push(comp);
addedSubComponents[comp["bom-ref"]] = true;
}
}
}
} // for
// Avoid creating empty component.components attribute
if (subComponents.length) {
parentComponent.components = subComponents;
} else {
parentComponent.components = undefined;
}
}
// Convert authors to author for specVersion < 1.6
const parentComponentCopy = { ...parentComponent };
if (options.specVersion < 1.6 && parentComponent?.authors) {
parentComponentCopy.author = parentComponentCopy.authors
.map((a) => (a.email ? `${a.name} <${a.email}>` : a.name))
.join(",");
delete parentComponentCopy.authors;
}
metadata.component = parentComponent;
}
// Have we already captured the oci properties
if (metadata?.properties?.some((prop) => prop.name === "oci:image:Id")) {
return metadata;
}
if (options) {
const mproperties = [];
if (options.exportData) {
const inspectData = options.exportData.inspectData;
if (inspectData) {
if (inspectData.Id) {
mproperties.push({
name: "oci:image:Id",
value: inspectData.Id,
});
}
if (
inspectData.RepoTags &&
Array.isArray(inspectData.RepoTags) &&
inspectData.RepoTags.length
) {
mproperties.push({
name: "oci:image:RepoTag",
value: inspectData.RepoTags[0],
});
}
if (
inspectData.RepoDigests &&
Array.isArray(inspectData.RepoDigests) &&
inspectData.RepoDigests.length
) {
mproperties.push({
name: "oci:image:RepoDigest",
value: inspectData.RepoDigests[0],
});
}
if (inspectData.Created) {
mproperties.push({
name: "oci:image:Created",
value: inspectData.Created,
});
}
if (inspectData.Architecture) {
mproperties.push({
name: "oci:image:Architecture",
value: inspectData.Architecture,
});
}
if (inspectData.Os) {
mproperties.push({
name: "oci:image:Os",
value: inspectData.Os,
});
}
}
const manifestList = options.exportData.manifest;
if (manifestList && Array.isArray(manifestList) && manifestList.length) {
const manifest = manifestList[0] || {};
if (manifest.Config) {
mproperties.push({
name: "oci:image:manifest:Config",
value: manifest.Config,
});
}
if (
manifest.Layers &&
Array.isArray(manifest.Layers) &&
manifest.Layers.length
) {
mproperties.push({
name: "oci:image:manifest:Layers",
value: manifest.Layers.join("\\n"),
});
}
}
const lastLayerConfig = options.exportData.lastLayerConfig;
if (lastLayerConfig) {
if (lastLayerConfig.id) {
mproperties.push({
name: "oci:image:lastLayer:Id",
value: lastLayerConfig.id,
});
}
if (lastLayerConfig.parent) {
mproperties.push({
name: "oci:image:lastLayer:ParentId",
value: lastLayerConfig.parent,
});
}
if (lastLayerConfig.created) {
mproperties.push({
name: "oci:image:lastLayer:Created",
value: lastLayerConfig.created,
});
}
}
const layerConfig =
lastLayerConfig?.config || options.exportData?.inspectData;
if (layerConfig) {
const env = layerConfig?.config?.Env || layerConfig?.Config?.Env;
if (env && Array.isArray(env) && env.length) {
mproperties.push({
name: "oci:image:lastLayer:Env",
value: env.join("\\n"),
});
// Does the image have any special packages that cdxgen cannot detect such as android-sdk and sdkman
const evalue = env.join(":");
if (
evalue.includes("android-sdk") ||
evalue.includes("commandlinetools")
) {
mproperties.push({
name: "oci:image:bundles:AndroidSdk",
value: "true",
});
}
// Track the use of special environment variables that could influence the search paths for libraries
// This list was generated by repeatedly prompting ChatGPT with examples.
// FIXME: Move these to a config file
for (const senvValue of [
"LD_LIBRARY_PATH",
"DYLD_LIBRARY_PATH",
"LD_PRELOAD",
"PYTHONPATH",
"CLASSPATH",
"PERL5LIB",
"PERLLIB",
"RUBYLIB",
"NODE_PATH",
"LUA_PATH",
"JULIA_LOAD_PATH",
"R_LIBS",
"R_LIBS_USER",
"GEM_PATH",
"DOTNET_ROOT",
"DOTNET_ADDITIONAL_DEPS",
"DOTNET_SHARED_STORE",
"DOTNET_STARTUP_HOOKS",
"DOTNET_BUNDLE_EXTRACT_BASE_DIR",
"JAVA_OPTIONS",
"JAVA_TOOL_OPTIONS",
"NODE_OPTIONS",
"PYTHONSTARTUP",
"RUBYOPT",
"WGETRC",
"APT_CONFIG",
"NPM_CONFIG_PREFIX",
"NPM_CONFIG_REGISTRY",
"YARN_CACHE_FOLDER",
"PNPM_STORE_PATH",
"PNPM_HOME",
"PNPM_CONFIG_",
"GIO_MODULE_DIR",
"GST_PLUGIN_PATH",
"GST_PLUGIN_SYSTEM_PATH",
"APPDIR_LIBRARY_PATH", // appimage specific which gets prepended to LD_LIBRARY_PATH
]) {
if (evalue.includes(senvValue)) {
mproperties.push({
name: `oci:image:env:${senvValue}`,
value: "true",
});
}
}
// This value represents a filtered and expanded path
if (options?.binPaths?.length) {
mproperties.push({
name: "oci:image:env:PATH",
value: options.binPaths.join(":"),
});
}
if (evalue.includes(".sdkman")) {
mproperties.push({
name: "oci:image:bundles:Sdkman",
value: "true",
});
}
if (evalue.includes(".nvm")) {
mproperties.push({
name: "oci:image:bundles:Nvm",
value: "true",
});
}
if (evalue.includes(".rbenv")) {
mproperties.push({
name: "oci:image:bundles:Rbenv",
value: "true",
});
}
}
const ccmd =
layerConfig?.config?.Cmd ||
layerConfig?.Config?.Cmd ||
layerConfig?.Config?.EntryPoint;
if (ccmd) {
if (Array.isArray(ccmd) && ccmd.length) {
const fullCommand = ccmd.join(" ");
mproperties.push({
name: "oci:image:lastLayer:Cmd",
value: fullCommand,
});
let appLanguage;
// TODO: Move these lists to a config file.
for (const lang in [
"java",
"node",
"dotnet",
"python",
"python3",
"ruby",
"php",
"php7",
"php8",
"perl",
]) {
if (fullCommand.includes(`${lang} `)) {
appLanguage = lang;
break;
}
}
if (appLanguage) {
mproperties.push({
name: "oci:image:appLanguage",
value: appLanguage,
});
}
} else {
mproperties.push({
name: "oci:image:lastLayer:Cmd",
value: ccmd.toString(),
});
}
}
}
}
if (options.allOSComponentTypes?.length) {
mproperties.push({
name: "oci:image:componentTypes",
value: options.allOSComponentTypes.sort().join("\\n"),
});
}
if (Number.isInteger(options?.unpackagedExecutableCount)) {
mproperties.push({
name: "cdx:container:unpackagedExecutableCount",
value: String(options.unpackagedExecutableCount),
});
}
if (Number.isInteger(options?.unpackagedSharedLibraryCount)) {
mproperties.push({
name: "cdx:container:unpackagedSharedLibraryCount",
value: String(options.unpackagedSharedLibraryCount),
});
}
// Should we move these to formulation?
if (options?.bundledSdks?.length) {
for (const sdk of options.bundledSdks) {
try {
const purlObj = PackageURL.fromString(sdk);
const sdkName = purlObj.name.split("-")[0].toLowerCase();
mproperties.push({
name: `oci:image:bundles:${sdkName}Sdk`,
value: "true",
});
} catch (_e) {
// ignore
}
}
}
if (options?.bundledRuntimes?.length) {
for (const runt of options.bundledRuntimes) {
mproperties.push({
name: `oci:image:bundles:${runt}Runtime`,
value: "true",
});
}
}
if (mproperties.length) {
metadata.properties = mproperties;
}
}
return metadata;
}
/**
* Method to create external references
*
* @param {Array | Object} opkg
* @returns {Array}
*/
function addExternalReferences(opkg) {
let externalReferences = [];
let pkgList;
if (Array.isArray(opkg)) {
pkgList = opkg;
} else {
pkgList = [opkg];
}
for (const pkg of pkgList) {
if (pkg.externalReferences) {
externalReferences = externalReferences.concat(pkg.externalReferences);
} else {
if (pkg.homepage?.url) {
externalReferences.push({
type: pkg.homepage.url.includes("git") ? "vcs" : "website",
url: pkg.homepage.url,
});
}
if (pkg.bugs?.url) {
externalReferences.push({
type: "issue-tracker",
url: pkg.bugs.url,
});
}
if (pkg.repository?.url) {
externalReferences.push({
type: "vcs",
url: pkg.repository.url,
});
}
if (pkg.distribution?.url) {
externalReferences.push({
type: "distribution",
url: pkg.distribution.url,
});
}
}
}
return externalReferences
.map((reference) => ({ ...reference, url: reference.url.trim() }))
.filter((reference) => isValidIriReference(reference.url));
}
/**
* For all modules in the specified package, creates a list of
* component objects from each one.
*
* @param {Object} options CLI options
* @param {Object} allImports All imports
* @param {Object} pkg Package object
* @param {string} ptype Package type
* @returns {Object[]} Array of component objects
*/
export function listComponents(options, allImports, pkg, ptype = "npm") {
const compMap = {};
const isRootPkg = ptype === "npm";
if (Array.isArray(pkg)) {
pkg.forEach((p) => {
addComponent(options, allImports, p, ptype, compMap, false);
});
} else {
addComponent(options, allImports, pkg, ptype, compMap, isRootPkg);
}
return Object.keys(compMap).map((k) => compMap[k]);
}
// These component types do not have PURLs
const NON_PURL_TYPES = ["cryptographic-asset", "file", "data"];
/**
* Given the specified package, create a CycloneDX component and add it to the list.
*/
function addComponent(
options,
allImports,
pkg,
ptype,
compMap,
isRootPkg = false,
) {
if (!pkg || pkg.extraneous) {
return;
}
if (!isRootPkg) {
const pkgIdentifier = parsePackageJsonName(pkg.name);
const author = pkg.author || undefined;
const authors = pkg.authors || undefined;
const publisher = pkg.publisher || undefined;
let group = pkg.group || pkgIdentifier.scope;
// Create empty group
group = group || "";
const name = pkgIdentifier.fullName || pkg.name || "";
// name is mandatory
if (!name) {
return;
}
// Do we need this still?
if (
!ptype &&
["jar", "war", "ear", "pom"].includes(pkg?.qualifiers?.type)
) {
ptype = "maven";
}
const version = pkg.version || "";
const licenses = pkg.licenses || getLicenses(pkg);
let purl = pkg.purl;
if (!purl && ptype) {
purl = new PackageURL(
ptype,
encodeForPurl(group),
encodeForPurl(name),
version,
pkg.qualifiers,
encodeForPurl(pkg.subpath),
);
}
let purlString = purl?.toString();
if (NON_PURL_TYPES.includes(ptype) || NON_PURL_TYPES.includes(pkg.type)) {
purl = undefined;
purlString = undefined;
}
// Some applications like github workflow steps and commands do not have purl
if (
pkg.purl === undefined &&
!pkg?.["bom-ref"]?.startsWith("pkg:") &&
pkg?.type === "application"
) {
purl = undefined;
purlString = undefined;
}
const description = pkg.description || undefined;
let compScope = pkg.scope;
if (allImports) {
const impPkgs = Object.keys(allImports);
if (
impPkgs.includes(name) ||
impPkgs.includes(`${group}/${name}`) ||
impPkgs.includes(`@${group}/${name}`) ||
impPkgs.includes(group) ||
impPkgs.includes(`@${group}`)
) {
compScope = "required";
} else if (impPkgs.length && compScope !== "excluded") {
compScope = "optional";
}
}
let component = {
author,
authors,
publisher,
group,
name,
version,
description,
scope: compScope,
hashes: [],
licenses,
purl: purlString,
externalReferences: addExternalReferences(pkg),
};
if (options.specVersion >= 1.5) {
component.pedigree = pkg.pedigree || undefined;
}
if (options.specVersion >= 1.6) {
component.releaseNotes = pkg.releaseNotes || undefined;
component.modelCard = pkg.modelCard || undefined;
component.data = pkg.data || undefined;
}
component["type"] = determinePackageType(pkg);
if (purlString) {
component["bom-ref"] = decodeURIComponent(purlString);
} else if (pkg["bom-ref"]) {
component["bom-ref"] = pkg["bom-ref"];
}
if (
component.externalReferences === undefined ||
component.externalReferences.length === 0
) {
delete component.externalReferences;
}
if (options.specVersion < 1.6) {
delete component.omniborId;
delete component.swhid;
}
processHashes(pkg, component);
// Upgrade authors section
if (options.specVersion >= 1.6 && component.author) {
const authorsList = [];
for (const aauthor of component.author.split(",")) {
authorsList.push({ name: aauthor });
}
component.authors = authorsList;
delete component.author;
}
// Downgrade authors section for < 1.5 :(
if (options.specVersion < 1.6) {
if (component?.authors?.length) {
component.author = component.authors
.map((a) => (a.email ? `${a.name} <${a.email}>` : a.name))
.join(",");
}
delete component.authors;
}
// Downgrade from 1.7
if (options.specVersion < 1.7) {
if (component.isExternal) {
delete component.isExternal;
}
if (component.versionRange) {
console.warn(
`Version Range is not supported in ${options.specVersion} specifications. Please run cdxgen with --spec-version 1.7`,
);
delete component.versionRange;
}
}
// Retain any tags
if (
options.specVersion >= 1.6 &&
pkg.tags &&
Object.keys(pkg.tags).length
) {
component.tags = pkg.tags;
}
// Retain any component properties and crypto properties
if (pkg.properties?.length) {
component.properties = pkg.properties;
}
if (pkg.cryptoProperties && typeof pkg.cryptoProperties === "object") {
component.cryptoProperties = pkg.cryptoProperties;
}
// Retain nested components
if (pkg.components) {
component.components = pkg.components;
}
component = enrichComponentWithMcpMetadata(component);
const compMapKey = component.purl || component["bom-ref"];
// Issue: 1353. We need to keep merging the properties
if (compMap[compMapKey]) {
const mergedComponents = trimComponents([compMap[compMapKey], component]);
if (mergedComponents?.length === 1) {
component = mergedComponents[0];
}
}
// Retain evidence
if (
options.specVersion >= 1.5 &&
pkg.evidence &&
Object.keys(pkg.evidence).length
) {
component.evidence = pkg.evidence;
// Convert evidence.identity section to an array for 1.6 and above
if (
options.specVersion >= 1.6 &&
pkg.evidence &&
pkg.evidence.identity &&
!Array.isArray(pkg.evidence.identity)
) {
// Automatically add concludedValue
if (pkg.evidence.identity?.methods?.length === 1) {
pkg.evidence.identity.concludedValue =
pkg.evidence.identity.methods[0].value;
}
component.evidence.identity = [pkg.evidence.identity];
}
// Convert evidence.identity section to an object for 1.5
if (
options.specVersion === 1.5 &&
pkg.evidence &&
pkg.evidence.identity &&
Array.isArray(pkg.evidence.identity)
) {
component.evidence.identity = pkg.evidence.identity[0];
}
}
compMap[compMapKey] = component;
}
if (pkg.dependencies) {
Object.keys(pkg.dependencies)
.map((x) => pkg.dependencies[x])
.filter((x) => typeof x !== "string") //remove cycles
.map((x) => addComponent(options, allImports, x, ptype, compMap, false));
}
}
/**
* If the author has described the module as a 'framework', the take their
* word for it, otherwise, identify the module as a 'library'.
*/
function determinePackageType(pkg) {
// Retain the exact component type in certain cases.
if (
[
"container",
"platform",
"operating-system",
"device",
"device-driver",
"firmware",
"file",
"machine-learning-model",
"data",
"cryptographic-asset",
].includes(pkg.type)
) {
return pkg.type;
}
if (pkg.type === "application") {
if (pkg?.name?.endsWith(".tar")) {
return "container";
}
return pkg.type;
}
if (pkg.purl) {
try {
const purl = PackageURL.fromString(pkg.purl);
if (purl.type) {
if (["docker", "oci", "container"].includes(purl.type)) {
return "container";
}
if (["github"].includes(purl.type)) {
return "application";
}
}
// See #1760
if (
purl.namespace?.startsWith("@types") ||
(purl.namespace?.includes("-types") && pkg?.type === "npm")
) {
return "library";
}
for (const cf of frameworksList.all) {
if (
pkg.purl.startsWith(cf) ||
purl.namespace?.includes(cf) ||
purl.name.toLowerCase().startsWith(cf)
) {
return "framework";
}
}
} catch (_e) {
// continue regardless of error
}
} else if (pkg.group) {
if (["actions"].includes(pkg.group)) {
return "application";
}
}
if (Object.hasOwn(pkg, "description")) {
if (pkg.description?.toLowerCase().includes("framework")) {
return "framework";
}
}
if (Object.hasOwn(pkg, "keywords")) {
for (const keyword of pkg.keywords) {
if (keyword && keyword.toLowerCase() === "framework") {
return "framework";
}
}
}
if (Object.hasOwn(pkg, "tags")) {
for (const tag of pkg.tags) {
if (tag && tag.toLowerCase() === "framework") {
return "framework";
}
}
}
return "library";
}
/**
* Uses the SHA1 shasum (if present) otherwise utilizes Subresource Integrity
* of the package with support for multiple hashing algorithms.
*/
function processHashes(pkg, component) {
if (pkg.hashes) {
// This attribute would be available when we read a bom json directly
// Eg: cyclonedx-maven-plugin. See: Bugs: #172, #175
for (const ahash of pkg.hashes) {
addComponentHash(ahash.alg, ahash.content, component);
}
} else if (pkg._shasum) {
const ahash = { alg: "SHA-1", content: pkg._shasum };
component.hashes.push(ahash);
} else if (pkg._integrity) {
const integrity = parse(pkg._integrity) || {};
// Components may have multiple hashes with various lengths. Check each one
// that is supported by the CycloneDX specification.
if (Object.hasOwn(integrity, "sha512")) {
addComponentHash("SHA-512", integrity.sha512[0].digest, component);
}
if (Object.hasOwn(integrity, "sha384")) {
addComponentHash("SHA-384", integrity.sha384[0].digest, component);
}
if (Object.hasOwn(integrity, "sha256")) {
addComponentHash("SHA-256", integrity.sha256[0].digest, component);
}
if (Object.hasOwn(integrity, "sha1")) {
addComponentHash("SHA-1", integrity.sha1[0].digest, component);
}
}
if (component.hashes.length === 0) {
delete component.hashes; // If no hashes exist, delete the hashes node (it's optional)
}
}
/**
* Adds a hash to component.
*/
function addComponentHash(alg, digest, component) {
let hash;
// If it is a valid hash simply use it
if (new RegExp(HASH_PATTERN).test(digest)) {
hash = digest;
} else {
// Check if base64 encoded
const isBase64Encoded =
Buffer.from(digest, "base64").toString("base64") === digest;
hash = isBase64Encoded
? Buffer.from(digest, "base64").toString("hex")
: digest;
}
const ahash = { alg: alg, content: hash };
component.hashes.push(ahash);
}
/**
* Return the BOM in json format including any namespace mapping
*
* @param {Object} options Options
* @param {Object} pkgInfo Package information
* @param {string} ptype Package type
* @param {Object} context Context
*
* @returns {Object} BOM with namespace mapping
*/
const buildBomNSData = (options, pkgInfo, ptype, context) => {
// Many create*Bom call sites provide only a source directory (`src`) when
// there is no single manifest/lock file to report, so activity records must
// fall back to that directory to keep the target populated.
const sourcePath =
context?.srcDir || context?.src || options.path || options.filePath;
const activityProjectType =
context?.projectType ||
(Array.isArray(options.projectType)
? options.projectType.length === 1
? options.projectType[0]
: undefined
: options.projectType);
setActivityContext({
packageType: ptype,
sourcePath,
...(activityProjectType ? { projectType: activityProjectType } : {}),
});
const bomNSData = {
bomJson: undefined,
bomJsonFiles: undefined,
nsMapping: undefined,
dependencies: undefined,
parentComponent: undefined,
};
const serialNum = `urn:uuid:${uuidv4()}`;
let allImports = {};
if (context?.allImports) {
allImports = context.allImports;
}
const nsMapping = context.nsMapping || {};
const dependencies = context.dependencies || [];
const services = context.services || [];
const parentComponent =
determineParentComponent(options) || context.parentComponent;
const metadata = addMetadata(parentComponent, options, context);
const components = listComponents(options, allImports, pkgInfo, ptype);
if (components && (components.length || parentComponent)) {
// CycloneDX Json Template
const jsonTpl = {
bomFormat: "CycloneDX",
specVersion: toCycloneDxSpecVersionString(options.specVersion || "1.7"),
serialNumber: serialNum,
version: 1,
metadata: metadata,
components,
dependencies,
};
setCycloneDxFormat(jsonTpl, jsonTpl.specVersion, {
preserveLegacyBomFormat: true,
});
if (services.length) {
jsonTpl.services = mergeServices([], services);
}
bomNSData.bomJson = jsonTpl;
bomNSData.nsMapping = nsMapping;
bomNSData.dependencies = dependencies;
bomNSData.parentComponent = parentComponent;
// Carry language-specific formulation data (e.g. Pixi) so that
// postProcess can merge it when building the final formulation section.
if (context?.formulationList?.length) {
bomNSData.formulationList = context.formulationList;
}
}
recordActivity({
kind: "read",
reason: `Collected ${ptype || "generic"} component metadata.`,
status: components?.length || parentComponent ? "completed" : "failed",
target: context?.filename || sourcePath,
});
resetActivityContext();
return bomNSData;
};
/**
* Function to create bom string for Java jars
*
* @param {string} path to the project
* @param {Object} options Parse options from the cli
*
* @returns {Object} BOM with namespace mapping
*/
export async function createJarBom(path, options) {
let pkgList = [];
let jarFiles;
let nsMapping = {};
const searchOptions = {
...options,
exclude: [...(options.exclude || [])],
};
if (typeof searchOptions.includeNodeModulesDir === "undefined") {
searchOptions.includeNodeModulesDir = shouldIncludeNodeModulesDir(options, [
"jar",
"war",
"ear",
]);
}
// Exclude certain directories during oci sbom generation
if (hasAnyProjectType(["oci"], options, false)) {
searchOptions.exclude.push("**/android-sdk*/**");
searchOptions.exclude.push("**/.sdkman/**");
}
const parentComponent = createDefaultParentComponent(path, "maven", options);
if (options.useGradleCache) {
nsMapping = await collectGradleDependencies(
getGradleCommand(path, null),
path,
false,
true,
);
} else if (options.useMavenCache) {
nsMapping = await collectMvnDependencies(
getMavenCommand(path, null),
null,
false,
true,
);
}
if (path.endsWith(".jar")) {
jarFiles = [resolve(path)];
} else {
jarFiles = getAllFiles(
path,
`${options.multiProject ? "**/" : ""}*.[jw]ar`,
searchOptions,
);
}
// Jenkins plugins
const hpiFiles = getAllFiles(
path,
`${options.multiProject ? "**/" : ""}*.hpi`,
searchOptions,
);
if (hpiFiles.length) {
jarFiles = jarFiles.concat(hpiFiles);
}
for (const jar of jarFiles) {
const tempDir = safeMkdtempSync(join(getTmpDir(), "jar-deps-"));
if (DEBUG_MODE) {
console.log(`Parsing ${jar}`);
}
const dlist = await extractJarArchive(jar, tempDir);
if (dlist?.length) {
pkgList = pkgList.concat(dlist);
}
if (pkgList.length) {
pkgList = await getMvnMetadata(pkgList);
}
// Clean up
if (tempDir?.startsWith(getTmpDir())) {
safeRmSync(tempDir, { recursive: true, force: true });
}
}
pkgList = pkgList.concat(await convertJarNSToPackages(nsMapping));
return buildBomNSData(options, pkgList, "maven", {
src: path,
parentComponent,
});
}
/**
* Function to create bom string for Android apps using blint
*
* @param {string} path to the project
* @param {Object} options Parse options from the cli
* @returns {Object|undefined} BOM object
*/
export function createAndroidBom(path, options) {
return createBinaryBom(path, options);
}
/**
* Function to create bom string for binaries using blint
*
* @param {string} path to the project
* @param {Object} options Parse options from the cli
* @returns {Object|undefined} BOM object
*/
export function createBinaryBom(path, options) {
const tempDir = safeMkdtempSync(join(getTmpDir(), "blint-tmp-"));
const binaryBomFile = join(tempDir, "bom.json");
getBinaryBom(path, binaryBomFile, options.deep);
if (safeExistsSync(binaryBomFile)) {
const binaryBom = JSON.parse(
readFileSync(binaryBomFile, { encoding: "utf-8" }),
);
attachIdentityTools(
binaryBom?.components,
extractToolRefs(
binaryBom?.metadata?.tools,
(tool) => tool?.name !== "cdxgen",
),
);
return {
bomJson: binaryBom,
dependencies: binaryBom.dependencies,
parentComponent: binaryBom.parentComponent,
};
}
return undefined;
}
/**
* Function to create bom string for Java projects
*
* @param {string} path to the project
* @param {Object} options Parse options from the cli
* @returns {Promise<Object>} Promise resolving to BOM object
*/
export async function createJavaBom(path, options) {
let jarNSMapping = {};
let pkgList = [];
let dependencies = [];
// cyclone-dx-maven plugin creates a component for the app under metadata
// This is subsequently referred to in the dependencies list
let parentComponent = {};
// Support for tracking all the tools that created the BOM
// For java, this would correctly include the cyclonedx maven plugin.
let tools;
let possible_misses = false;
// war/ear mode
if (path.endsWith(".war") || path.endsWith(".jar")) {
// Check if the file exists
if (safeExistsSync(path)) {
if (DEBUG_MODE) {
console.log(`Retrieving packages from ${path}`);
}
const tempDir = safeMkdtempSync(join(getTmpDir(), "war-deps-"));
jarNSMapping = await collectJarNS(tempDir);
pkgList = await extractJarArchive(path, tempDir, jarNSMapping);
if (pkgList.length) {
pkgList = await getMvnMetadata(pkgList);
}
// Clean up
if (tempDir?.startsWith(getTmpDir())) {
console.log(`Cleaning up ${tempDir}`);
safeRmSync(tempDir, { recursive: true, force: true });
}
} else {
console.log(`${path} doesn't exist`);
}
return buildBomNSData(options, pkgList, "maven", {
src: dirname(path),
filename: path,
nsMapping: jarNSMapping,
dependencies,
parentComponent,
});
}
// -t quarkus is supported
let isQuarkus = options?.projectType?.includes("quarkus");
let useMavenDepsTree = isQuarkus ? false : PREFER_MAVEN_DEPS_TREE;
// Is this a multi-module project
let rootModules;
// maven - pom.xml
const pomFiles = getAllFiles(
path,
`${options.multiProject ? "**/" : ""}pom.xml`,
options,
);
// gradle
const gradleFiles = getAllFiles(
path,
`${options.multiProject ? "**/" : ""}build.gradle*`,
options,
);
// mill
const millFiles = getAllFiles(
path,
`${options.multiProject ? "**/" : ""}build.mill`,
options,
);
let bomJsonFiles = [];
if (
pomFiles?.length &&
isPackageManagerAllowed(
"maven",
["bazel", "sbt", "gradle", "mill"],
options,
)
) {
if (gradleFiles.length) {
thoughtLog(
`Is this a Gradle project? I recommend invoking cdxgen with the "-t gradle" option if you're encountering build errors.`,
);
}
if (!isQuarkus) {
// Quarkus projects require special treatment. To detect quarkus, we parse the first 3 maven file to look for a hit
for (const pf of pomFiles.slice(0, 3)) {
const pomMap = parsePom(pf);
if (!rootModules && pomMap?.modules?.length) {
rootModules = pomMap.modules;
}
// In quarkus mode, we cannot use the maven deps tree
if (pomMap.isQuarkus) {
isQuarkus = true;
useMavenDepsTree = false;
break;
}
}
}
let result;
let mvnArgs;
// FIXME: How do we motivate everyone to upgrade to 1.7?
const toolsSpecVersion = 1.6;
if (isQuarkus) {
thoughtLog(
"This appears to be a Quarkus project. Let's use the right Maven plugin.",
);
// disable analytics. See: https://quarkus.io/usage/
mvnArgs = [
"-fn",
"quarkus:dependency-sbom",
"-Dquarkus.analytics.disabled=true",
];
if (options.specVersion >= 1.6) {
mvnArgs = mvnArgs.concat(
`-Dquarkus.dependency.sbom.schema-version=${toolsSpecVersion}`,
);
}
} else {
// FIXME: The last maven plugin release was on November 28th, 2024.
// Should we fork this repo and maintain it ourselves?
const cdxMavenPlugin =
process.env.CDX_MAVEN_PLUGIN ||
"org.cyclonedx:cyclonedx-maven-plugin:2.9.1";
const cdxMavenGoal = process.env.CDX_MAVEN_GOAL || "makeAggregateBom";
mvnArgs = [
"-fn",
`${cdxMavenPlugin}:${cdxMavenGoal}`,
"-DoutputName=bom",
];
if (includeMavenTestScope) {
mvnArgs.push("-DincludeTestScope=true");
}
// By using quiet mode we can reduce the maxBuffer used and avoid crashes