@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
804 lines (759 loc) • 22.8 kB
JavaScript
import {
mergeDependencies,
mergeServices,
trimComponents,
} from "./depsUtils.js";
import { getHbomHardwareClass, isHbomLikeBom } from "./hbomAnalysis.js";
import { getPropertyValue } from "./inventoryStats.js";
const HOST_VIEW_PROPERTY_PREFIX = "cdx:hostview:";
const NETWORK_HARDWARE_CLASSES = new Set([
"network-interface",
"wireless-adapter",
]);
const STORAGE_HARDWARE_CLASSES = new Set([
"storage",
"storage-device",
"storage-volume",
]);
const OBOM_HOST_ANCHOR_TYPES = new Set(["device", "operating-system"]);
const HBOM_NETWORK_IDENTITY_PROPS = Object.freeze([
"cdx:hbom:interface",
"cdx:hbom:interfaceName",
]);
const RUNTIME_STORAGE_IDENTITY_PROPS = Object.freeze([
"volume_uuid",
"persistent_volume_id",
"uuid",
"device_id",
]);
const HBOM_STORAGE_IDENTITY_PROPS = Object.freeze([
"cdx:hbom:volumeUuid",
"cdx:hbom:uuid",
"cdx:hbom:deviceNode",
]);
const HBOM_STORAGE_DEVICE_PROPS = Object.freeze([
"cdx:hbom:deviceNode",
"cdx:hbom:devicePath",
]);
const HBOM_STORAGE_MOUNT_PATH_PROPS = Object.freeze([
"cdx:hbom:mountPoint",
"cdx:hbom:mountPath",
"cdx:hbom:path",
]);
const SECURE_BOOT_HOST_IDENTITY_PROPS = Object.freeze([
[
"path",
Object.freeze([
"cdx:hbom:secureBootCertificatePath",
"cdx:hbom:secureBootDbPath",
"cdx:hbom:secureBootDbxPath",
]),
],
[
"serial",
Object.freeze([
"cdx:hbom:secureBootCertificateSerial",
"cdx:hbom:secureBootDbSerial",
"cdx:hbom:secureBootDbxSerial",
"cdx:hbom:secureBootSerial",
]),
],
[
"sha1",
Object.freeze([
"cdx:hbom:secureBootCertificateSha1",
"cdx:hbom:secureBootDbSha1",
"cdx:hbom:secureBootDbxSha1",
"cdx:hbom:secureBootSha1",
]),
],
[
"subject_key_id",
Object.freeze([
"cdx:hbom:secureBootCertificateSubjectKeyId",
"cdx:hbom:secureBootDbSubjectKeyId",
"cdx:hbom:secureBootDbxSubjectKeyId",
"cdx:hbom:secureBootSubjectKeyId",
]),
],
[
"authority_key_id",
Object.freeze([
"cdx:hbom:secureBootCertificateAuthorityKeyId",
"cdx:hbom:secureBootDbAuthorityKeyId",
"cdx:hbom:secureBootDbxAuthorityKeyId",
"cdx:hbom:secureBootAuthorityKeyId",
]),
],
]);
function getPropertyValues(propertiesOrObject, propertyName) {
const properties = Array.isArray(propertiesOrObject)
? propertiesOrObject
: Array.isArray(propertiesOrObject?.properties)
? propertiesOrObject.properties
: [];
return properties
.filter((property) => property?.name === propertyName)
.map((property) => property.value)
.filter(
(value) =>
value !== undefined && value !== null && `${value}`.trim() !== "",
);
}
function removePropertiesByPrefix(subject, propertyPrefix) {
if (!Array.isArray(subject?.properties)) {
return;
}
subject.properties = subject.properties.filter(
(property) => !`${property?.name || ""}`.startsWith(propertyPrefix),
);
}
function addUniqueProperty(subject, name, value) {
if (
!subject ||
value === undefined ||
value === null ||
`${value}`.trim() === ""
) {
return;
}
if (!Array.isArray(subject.properties)) {
subject.properties = [];
}
if (
subject.properties.some(
(property) => property?.name === name && property?.value === `${value}`,
)
) {
return;
}
subject.properties.push({
name,
value: `${value}`,
});
}
function addHostViewSummaryProperty(bomJson, name, value) {
addUniqueProperty(bomJson, name, value);
addUniqueProperty(bomJson?.metadata?.component, name, value);
}
function sanitizeRefToken(value, fallback = "unknown") {
let normalizedValue = `${value || ""}`
.trim()
.replace(/[\r\n\t]+/gu, " ")
.replace(/\s+/gu, "-")
.replace(/[^a-zA-Z0-9._:-]+/gu, "-")
.replace(/-+/gu, "-");
while (normalizedValue.startsWith("-") || normalizedValue.startsWith(":")) {
normalizedValue = normalizedValue.slice(1);
}
while (normalizedValue.endsWith("-") || normalizedValue.endsWith(":")) {
normalizedValue = normalizedValue.slice(0, -1);
}
return normalizedValue || fallback;
}
function createSyntheticMetadataRef(metadataComponent) {
return [
"urn:cdxgen:host",
sanitizeRefToken(metadataComponent?.type || "device"),
sanitizeRefToken(metadataComponent?.name || "host"),
sanitizeRefToken(
getPropertyValue(metadataComponent, "cdx:hbom:platform") ||
getPropertyValue(metadataComponent, "cdx:hbom:architecture") ||
metadataComponent?.version ||
"live",
),
].join(":");
}
function createSyntheticComponentRef(component) {
const hbomClass = getHbomHardwareClass(component);
const runtimeCategory = getPropertyValue(component, "cdx:osquery:category");
const identityValue =
getPropertyValue(component, "cdx:hbom:busInfo") ||
getPropertyValue(component, "interface") ||
getPropertyValue(component, "path") ||
getPropertyValue(component, "uuid") ||
component?.version ||
"unknown";
return [
"urn:cdxgen:component",
sanitizeRefToken(component?.type || "component"),
sanitizeRefToken(hbomClass || runtimeCategory || "component"),
sanitizeRefToken(component?.name || "unnamed"),
sanitizeRefToken(identityValue),
].join(":");
}
function assignSyntheticBomRef(subject, usedRefs, createRef) {
if (!subject || typeof subject !== "object") {
return;
}
const existingRef = subject["bom-ref"];
if (existingRef) {
usedRefs.add(existingRef.toLowerCase());
return;
}
const baseRef = createRef(subject);
let candidateRef = baseRef;
let collisionCounter = 1;
while (usedRefs.has(candidateRef.toLowerCase())) {
collisionCounter += 1;
candidateRef = `${baseRef}:${collisionCounter}`;
}
subject["bom-ref"] = candidateRef;
usedRefs.add(candidateRef.toLowerCase());
}
function ensureBomRefs(bomJson) {
const usedRefs = new Set();
if (bomJson?.metadata?.component?.["bom-ref"]) {
usedRefs.add(bomJson.metadata.component["bom-ref"].toLowerCase());
}
for (const component of bomJson?.components || []) {
if (component?.["bom-ref"]) {
usedRefs.add(component["bom-ref"].toLowerCase());
}
}
if (bomJson?.metadata?.component) {
assignSyntheticBomRef(
bomJson.metadata.component,
usedRefs,
createSyntheticMetadataRef,
);
}
for (const component of bomJson?.components || []) {
assignSyntheticBomRef(component, usedRefs, createSyntheticComponentRef);
}
return bomJson;
}
function appendMapEntry(map, key, value) {
if (!key || !value) {
return;
}
if (!map.has(key)) {
map.set(key, new Set());
}
map.get(key).add(value);
}
function toNormalizedIdentity(value) {
const normalizedValue = `${value || ""}`.trim().toLowerCase();
if (!normalizedValue || normalizedValue.startsWith("redacted:")) {
return undefined;
}
return normalizedValue;
}
function getNormalizedPropertyValues(subject, propertyName) {
return getPropertyValues(subject, propertyName)
.map((value) => toNormalizedIdentity(value))
.filter(Boolean);
}
function getNormalizedCandidateValues(values = []) {
return Array.from(
new Set(values.map((value) => toNormalizedIdentity(value)).filter(Boolean)),
);
}
function isFilesystemIdentity(value) {
const normalizedValue = `${value || ""}`.trim();
return (
normalizedValue.startsWith("/") ||
/^[a-z]:$/iu.test(normalizedValue) ||
/^[a-z]:[\\/]/iu.test(normalizedValue)
);
}
function createRuntimeIndexes(runtimeComponents) {
const interfaceAddressesByName = new Map();
const kernelModulesByName = new Map();
const mountPathsByDevice = new Map();
const mountPathsByPath = new Map();
const runtimeByStorageIdentity = new Map();
const secureBootCertificatesByField = {
authority_key_id: new Map(),
path: new Map(),
serial: new Map(),
sha1: new Map(),
subject_key_id: new Map(),
};
for (const component of runtimeComponents) {
const runtimeCategory = getPropertyValue(component, "cdx:osquery:category");
if (!runtimeCategory || !component?.["bom-ref"]) {
continue;
}
if (runtimeCategory === "interface_addresses") {
appendMapEntry(
interfaceAddressesByName,
toNormalizedIdentity(getPropertyValue(component, "interface")),
component["bom-ref"],
);
}
if (runtimeCategory === "kernel_modules") {
appendMapEntry(
kernelModulesByName,
toNormalizedIdentity(component.name),
component["bom-ref"],
);
}
if (runtimeCategory === "mount_hardening") {
appendMapEntry(
mountPathsByDevice,
toNormalizedIdentity(getPropertyValue(component, "device")),
component["bom-ref"],
);
appendMapEntry(
mountPathsByPath,
toNormalizedIdentity(getPropertyValue(component, "path")),
component["bom-ref"],
);
}
if (runtimeCategory === "secureboot_certificates") {
for (const fieldName of Object.keys(secureBootCertificatesByField)) {
appendMapEntry(
secureBootCertificatesByField[fieldName],
toNormalizedIdentity(getPropertyValue(component, fieldName)),
component["bom-ref"],
);
}
}
for (const propertyName of RUNTIME_STORAGE_IDENTITY_PROPS) {
appendMapEntry(
runtimeByStorageIdentity,
toNormalizedIdentity(getPropertyValue(component, propertyName)),
component["bom-ref"],
);
}
}
return {
interfaceAddressesByName,
kernelModulesByName,
mountPathsByDevice,
mountPathsByPath,
runtimeByStorageIdentity,
secureBootCertificatesByField,
};
}
function addCategoryRefs(linkedRefs, linkedCategoryRefs, category, refs = []) {
if (!category || !refs?.length) {
return;
}
if (!linkedCategoryRefs.has(category)) {
linkedCategoryRefs.set(category, new Set());
}
const categoryRefs = linkedCategoryRefs.get(category);
for (const ref of refs) {
if (!ref) {
continue;
}
linkedRefs.add(ref);
categoryRefs.add(ref);
}
}
function annotateLinkedComponent(component, linkedCategoryRefs, linkedRefs) {
removePropertiesByPrefix(component, HOST_VIEW_PROPERTY_PREFIX);
const sortedCategories = Array.from(linkedCategoryRefs.keys()).sort();
addUniqueProperty(
component,
"cdx:hostview:linkedRuntimeCategoryCount",
`${sortedCategories.length}`,
);
addUniqueProperty(
component,
"cdx:hostview:topologyLinkCount",
`${linkedRefs.size}`,
);
if (linkedCategoryRefs.has("interface_addresses")) {
addUniqueProperty(
component,
"cdx:hostview:runtimeAddressCount",
`${linkedCategoryRefs.get("interface_addresses").size}`,
);
}
for (const category of sortedCategories) {
addUniqueProperty(
component,
"cdx:hostview:linkedRuntimeCategory",
category,
);
addUniqueProperty(
component,
`cdx:hostview:${category}:count`,
`${linkedCategoryRefs.get(category).size}`,
);
}
}
function createHostTrustLinks(metadataComponent, indexes) {
const linkedRefs = new Set();
const linkedCategoryRefs = new Map();
for (const [fieldName, propertyNames] of SECURE_BOOT_HOST_IDENTITY_PROPS) {
const fieldIndex = indexes.secureBootCertificatesByField[fieldName];
if (!fieldIndex) {
continue;
}
for (const propertyName of propertyNames) {
const identityValues = getNormalizedPropertyValues(
metadataComponent,
propertyName,
);
for (const identityValue of identityValues) {
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"secureboot_certificates",
Array.from(fieldIndex.get(identityValue) || []),
);
}
}
}
return {
linkedCategoryRefs,
linkedRefs,
};
}
function createDependencyEdgeList(
hostRef,
metadataComponent,
hbomComponents,
runtimeAnchorRefs,
indexes,
) {
const newDependencies = [];
const linkedRuntimeCategories = new Set();
let linkedHardwareComponentCount = 0;
let topologyLinkCount = 0;
const hostTrustLinks = createHostTrustLinks(metadataComponent, indexes);
const hostDependsOn = [
...hbomComponents.map((component) => component["bom-ref"]),
...runtimeAnchorRefs,
...hostTrustLinks.linkedRefs,
].sort();
if (hostRef && hostDependsOn.length) {
newDependencies.push({
ref: hostRef,
dependsOn: hostDependsOn,
});
}
if (hostTrustLinks.linkedRefs.size) {
topologyLinkCount += hostTrustLinks.linkedRefs.size;
for (const runtimeCategory of hostTrustLinks.linkedCategoryRefs.keys()) {
linkedRuntimeCategories.add(runtimeCategory);
}
annotateLinkedComponent(
metadataComponent,
hostTrustLinks.linkedCategoryRefs,
hostTrustLinks.linkedRefs,
);
}
for (const component of hbomComponents) {
const linkedRefs = new Set();
const linkedCategoryRefs = new Map();
const hardwareClass = getHbomHardwareClass(component);
if (NETWORK_HARDWARE_CLASSES.has(hardwareClass)) {
const interfaceKeys = getNormalizedCandidateValues([
component.name,
component.version,
...HBOM_NETWORK_IDENTITY_PROPS.flatMap((propertyName) =>
getPropertyValues(component, propertyName),
),
]);
for (const interfaceKey of interfaceKeys) {
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"interface_addresses",
Array.from(indexes.interfaceAddressesByName.get(interfaceKey) || []),
);
}
const driverKey = toNormalizedIdentity(
getPropertyValue(component, "cdx:hbom:driver"),
);
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"kernel_modules",
Array.from(indexes.kernelModulesByName.get(driverKey) || []),
);
}
if (STORAGE_HARDWARE_CLASSES.has(hardwareClass)) {
for (const propertyName of HBOM_STORAGE_IDENTITY_PROPS) {
const storageIdentity = toNormalizedIdentity(
getPropertyValue(component, propertyName),
);
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"runtime-storage",
Array.from(
indexes.runtimeByStorageIdentity.get(storageIdentity) || [],
),
);
}
for (const propertyName of HBOM_STORAGE_DEVICE_PROPS) {
const devicePath = toNormalizedIdentity(
getPropertyValue(component, propertyName),
);
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"mount_hardening",
Array.from(indexes.mountPathsByDevice.get(devicePath) || []),
);
}
for (const propertyName of HBOM_STORAGE_MOUNT_PATH_PROPS) {
const mountPath = toNormalizedIdentity(
getPropertyValue(component, propertyName),
);
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"mount_hardening",
Array.from(indexes.mountPathsByPath.get(mountPath) || []),
);
}
const explicitFilesystemIdentity = isFilesystemIdentity(component.version)
? toNormalizedIdentity(component.version)
: undefined;
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"mount_hardening",
Array.from(
indexes.mountPathsByDevice.get(explicitFilesystemIdentity) || [],
),
);
addCategoryRefs(
linkedRefs,
linkedCategoryRefs,
"mount_hardening",
Array.from(
indexes.mountPathsByPath.get(explicitFilesystemIdentity) || [],
),
);
}
if (!linkedRefs.size) {
removePropertiesByPrefix(component, HOST_VIEW_PROPERTY_PREFIX);
continue;
}
linkedHardwareComponentCount += 1;
topologyLinkCount += linkedRefs.size;
for (const runtimeCategory of linkedCategoryRefs.keys()) {
linkedRuntimeCategories.add(runtimeCategory);
}
annotateLinkedComponent(component, linkedCategoryRefs, linkedRefs);
newDependencies.push({
ref: component["bom-ref"],
dependsOn: Array.from(linkedRefs).sort(),
});
}
return {
linkedHardwareComponentCount,
linkedRuntimeCategories: Array.from(linkedRuntimeCategories).sort(),
newDependencies,
topologyLinkCount,
};
}
function mergeToolComponents(firstComponents = [], secondComponents = []) {
const mergedByKey = new Map();
for (const component of [...firstComponents, ...secondComponents]) {
if (!component) {
continue;
}
const key =
`${component["bom-ref"] || ""}:${component.group || ""}:${component.name || ""}:${component.version || ""}`.toLowerCase();
if (!mergedByKey.has(key)) {
mergedByKey.set(key, component);
}
}
return Array.from(mergedByKey.values());
}
function mergeMetadataProperties(firstProperties = [], secondProperties = []) {
const mergedProperties = [];
for (const property of [...firstProperties, ...secondProperties]) {
if (
!property?.name ||
property?.value === undefined ||
property?.value === null
) {
continue;
}
if (
!mergedProperties.find(
(existingProperty) =>
existingProperty.name === property.name &&
existingProperty.value === property.value,
)
) {
mergedProperties.push(property);
}
}
return mergedProperties;
}
export function isMergedHostViewBom(bomJson) {
return (
getPropertyValue(bomJson, "cdx:hostview:mode") === "hbom-obom-merged" ||
(isHbomLikeBom(bomJson) &&
(bomJson?.components || []).some((component) =>
getPropertyValue(component, "cdx:osquery:category"),
))
);
}
export function getHostViewSummary(bomJson) {
return {
linkedHardwareComponentCount: Number.parseInt(
`${getPropertyValue(bomJson, "cdx:hostview:linkedHardwareComponentCount") || 0}`,
10,
),
linkedRuntimeCategories: getPropertyValues(
bomJson,
"cdx:hostview:linkedRuntimeCategory",
),
mode: getPropertyValue(bomJson, "cdx:hostview:mode"),
runtimeAnchorCount: Number.parseInt(
`${getPropertyValue(bomJson, "cdx:hostview:runtimeAnchorCount") || 0}`,
10,
),
runtimeComponentCount: Number.parseInt(
`${getPropertyValue(bomJson, "cdx:hostview:runtimeComponentCount") || 0}`,
10,
),
topologyLinkCount: Number.parseInt(
`${getPropertyValue(bomJson, "cdx:hostview:topologyLinkCount") || 0}`,
10,
),
};
}
function ensureHostViewCategoryProperties(bomJson, linkedRuntimeCategories) {
for (const runtimeCategory of linkedRuntimeCategories || []) {
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:linkedRuntimeCategory",
runtimeCategory,
);
}
}
export function applyHostInventoryTopology(bomJson) {
if (!bomJson || !isHbomLikeBom(bomJson)) {
return bomJson;
}
ensureBomRefs(bomJson);
removePropertiesByPrefix(bomJson, HOST_VIEW_PROPERTY_PREFIX);
removePropertiesByPrefix(
bomJson.metadata?.component,
HOST_VIEW_PROPERTY_PREFIX,
);
const runtimeComponents = (bomJson.components || []).filter((component) =>
getPropertyValue(component, "cdx:osquery:category"),
);
const hbomComponents = (bomJson.components || []).filter((component) =>
getHbomHardwareClass(component),
);
const runtimeAnchorRefs = (bomJson.components || [])
.filter(
(component) =>
component?.["bom-ref"] &&
OBOM_HOST_ANCHOR_TYPES.has(component.type) &&
getPropertyValue(component, "cdx:osquery:category") === undefined &&
getHbomHardwareClass(component) === undefined,
)
.map((component) => component["bom-ref"]);
const indexes = createRuntimeIndexes(runtimeComponents);
const hostRef = bomJson?.metadata?.component?.["bom-ref"];
const {
linkedHardwareComponentCount,
linkedRuntimeCategories,
newDependencies,
topologyLinkCount,
} = createDependencyEdgeList(
hostRef,
bomJson?.metadata?.component,
hbomComponents,
runtimeAnchorRefs,
indexes,
);
bomJson.dependencies = mergeDependencies(
bomJson.dependencies || [],
newDependencies,
);
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:mode",
runtimeComponents.length ? "hbom-obom-merged" : "hbom-topology",
);
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:hardwareComponentCount",
`${hbomComponents.length}`,
);
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:runtimeComponentCount",
`${runtimeComponents.length}`,
);
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:runtimeAnchorCount",
`${runtimeAnchorRefs.length}`,
);
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:linkedHardwareComponentCount",
`${linkedHardwareComponentCount}`,
);
addHostViewSummaryProperty(
bomJson,
"cdx:hostview:topologyLinkCount",
`${topologyLinkCount}`,
);
ensureHostViewCategoryProperties(bomJson, linkedRuntimeCategories);
return bomJson;
}
export function mergeHostInventoryBoms(hbomJson, obomData) {
if (!hbomJson) {
return hbomJson;
}
if (!obomData?.bomJson) {
return applyHostInventoryTopology(hbomJson);
}
const runtimeComponents = [...(obomData.bomJson.components || [])];
if (obomData.parentComponent?.name && obomData.parentComponent?.type) {
runtimeComponents.unshift(obomData.parentComponent);
}
const mergedBomJson = {
...hbomJson,
components: trimComponents([
...(hbomJson.components || []),
...runtimeComponents,
]),
dependencies: mergeDependencies(
hbomJson.dependencies || [],
obomData.bomJson.dependencies || [],
),
services: mergeServices(
hbomJson.services || [],
obomData.bomJson.services || [],
),
metadata: {
...hbomJson.metadata,
lifecycles: Array.from(
new Set([
...(hbomJson.metadata?.lifecycles || []).map((entry) =>
JSON.stringify(entry),
),
...(obomData.bomJson.metadata?.lifecycles || []).map((entry) =>
JSON.stringify(entry),
),
]),
).map((entry) => JSON.parse(entry)),
properties: mergeMetadataProperties(
hbomJson.metadata?.properties || [],
obomData.bomJson.metadata?.properties || [],
),
tools: {
...(hbomJson.metadata?.tools || {}),
components: mergeToolComponents(
hbomJson.metadata?.tools?.components || [],
obomData.bomJson.metadata?.tools?.components || [],
),
},
},
properties: mergeMetadataProperties(
hbomJson.properties || [],
obomData.bomJson.properties || [],
),
};
return applyHostInventoryTopology(mergedBomJson);
}