@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
1,004 lines (975 loc) • 29 kB
JavaScript
function appendProperty(properties, name, value) {
if (!name || value === undefined || value === null || value === "") {
return;
}
properties.push({
name,
value: typeof value === "string" ? value : String(value),
});
}
function uniqueStrings(values) {
return [
...new Set(values.filter(Boolean).map((value) => String(value).trim())),
];
}
function parseTimestamp(value) {
if (!value || typeof value !== "string") {
return undefined;
}
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? undefined : timestamp;
}
function sortReleaseEntries(entries) {
return entries.sort((left, right) => left.timestamp - right.timestamp);
}
function median(numbers) {
if (!numbers.length) {
return undefined;
}
const sorted = [...numbers].sort((left, right) => left - right);
const midpoint = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[midpoint - 1] + sorted[midpoint]) / 2;
}
return sorted[midpoint];
}
function normalizeIdentity(value) {
if (!value) {
return undefined;
}
return String(value).trim().toLowerCase();
}
function uniqueIdentities(values) {
return [
...new Set(values.map((value) => normalizeIdentity(value)).filter(Boolean)),
];
}
function isDisjointIdentitySet(leftSet, rightSet) {
if (!leftSet.length || !rightSet.length) {
return false;
}
return leftSet.every(
(leftValue) => !rightSet.some((rightValue) => rightValue === leftValue),
);
}
function identityOverlapMetrics(leftSet, rightSet) {
if (!leftSet.length || !rightSet.length) {
return {};
}
const rightValues = new Set(rightSet);
const overlapCount = leftSet.filter((leftValue) =>
rightValues.has(leftValue),
).length;
const unionCount = new Set([...leftSet, ...rightSet]).size;
return {
overlapCount,
overlapRatio: unionCount > 0 ? overlapCount / unionCount : undefined,
partialDrift:
overlapCount > 0 &&
overlapCount < unionCount &&
(overlapCount < leftSet.length || overlapCount < rightSet.length),
};
}
function extractMaintainerIdentities(maintainers) {
if (!Array.isArray(maintainers)) {
return [];
}
const identities = [];
for (const maintainer of maintainers) {
if (typeof maintainer === "string") {
identities.push(maintainer);
continue;
}
identities.push(maintainer?.name, maintainer?.email);
}
return uniqueIdentities(identities);
}
function releaseGapMetrics(releaseEntries, currentVersion) {
const sortedEntries = sortReleaseEntries([...releaseEntries]);
const currentIndex = sortedEntries.findIndex(
(entry) => entry.version === currentVersion,
);
if (currentIndex < 1) {
return {};
}
const currentGapDays =
(sortedEntries[currentIndex].timestamp -
sortedEntries[currentIndex - 1].timestamp) /
(1000 * 60 * 60 * 24);
const priorGapDays = [];
for (let index = 1; index < currentIndex; index += 1) {
priorGapDays.push(
(sortedEntries[index].timestamp - sortedEntries[index - 1].timestamp) /
(1000 * 60 * 60 * 24),
);
}
return {
baselineDays: median(priorGapDays),
currentGapDays,
sampleSize: priorGapDays.length,
};
}
function compressedCadenceMetrics(gapMetrics) {
const baselineDays = gapMetrics?.baselineDays;
const currentGapDays = gapMetrics?.currentGapDays;
const sampleSize = gapMetrics?.sampleSize;
if (
baselineDays === undefined ||
currentGapDays === undefined ||
sampleSize === undefined ||
sampleSize < 3 ||
baselineDays <= 0 ||
currentGapDays <= 0
) {
return {};
}
const compressionRatio = currentGapDays / baselineDays;
return {
compressedCadence:
baselineDays >= 21 && currentGapDays <= 14 && compressionRatio <= 0.33,
compressionRatio,
};
}
function extractNestedValue(obj, paths) {
for (const path of paths) {
let current = obj;
for (const segment of path) {
current = current?.[segment];
if (current === undefined || current === null) {
break;
}
}
if (current !== undefined && current !== null && current !== "") {
return current;
}
}
return undefined;
}
function normalizeProvenanceUrl(value) {
if (!value) {
return undefined;
}
if (typeof value === "string") {
return value;
}
return extractNestedValue(value, [
["url"],
["provenanceUrl"],
["attestationUrl"],
["bundle", "url"],
["provenance", "url"],
["attestations", "url"],
]);
}
function collectPathValues(value, pathSegments) {
if (value === undefined || value === null) {
return [];
}
if (!pathSegments.length) {
if (Array.isArray(value)) {
return value.flatMap((entry) => collectPathValues(entry, []));
}
return [value];
}
if (Array.isArray(value)) {
return value.flatMap((entry) => collectPathValues(entry, pathSegments));
}
const [currentSegment, ...remainingSegments] = pathSegments;
return collectPathValues(value?.[currentSegment], remainingSegments);
}
function normalizeCollectedValues(values) {
const normalizedValues = [];
for (const value of values) {
if (value === undefined || value === null || value === "") {
continue;
}
if (typeof value === "string") {
normalizedValues.push(value);
continue;
}
if (typeof value === "number" || typeof value === "boolean") {
normalizedValues.push(String(value));
}
}
return uniqueStrings(normalizedValues);
}
function collectNestedValues(value, paths) {
const collectedValues = [];
for (const path of paths) {
collectedValues.push(...collectPathValues(value, path));
}
return normalizeCollectedValues(collectedValues);
}
function appendJoinedProperty(properties, name, values) {
appendProperty(properties, name, uniqueStrings(values).join(", "));
}
function collectProvenanceDigests(value) {
return collectNestedValues(value, [
["digest"],
["hash"],
["sha256"],
["sha512"],
["integrity"],
["hashes", "sha256"],
["hashes", "sha512"],
["subject", "digest", "sha256"],
["subject", "digest", "sha512"],
["statement", "subject", "digest", "sha256"],
["statement", "subject", "digest", "sha512"],
["bundle", "subject", "digest", "sha256"],
["bundle", "subject", "digest", "sha512"],
]);
}
function collectProvenanceKeyIds(value) {
return collectNestedValues(value, [
["keyid"],
["keyId"],
["publicKeyId"],
["verificationKeyId"],
["signingKeyId"],
["signatures", "keyid"],
["signatures", "keyId"],
["verificationMaterial", "publicKey", "keyid"],
["verificationMaterial", "publicKey", "keyId"],
["verificationMaterial", "certificate", "keyid"],
["verificationMaterial", "certificate", "keyId"],
]);
}
function collectProvenanceSignatures(value) {
return collectNestedValues(value, [
["signature"],
["sig"],
["signatures", "sig"],
["signatures", "signature"],
]);
}
function collectProvenancePredicateTypes(value) {
return collectNestedValues(value, [
["predicateType"],
["predicate_type"],
["statement", "predicateType"],
["bundle", "predicateType"],
]);
}
function hasTrustedPublishingEvidence(value) {
if (!value) {
return false;
}
if (typeof value === "boolean") {
return value;
}
if (typeof value === "string") {
return /(trusted|oidc|attestation|provenance)/i.test(value);
}
if (Array.isArray(value)) {
return value.some((entry) => hasTrustedPublishingEvidence(entry));
}
return Boolean(
normalizeProvenanceUrl(value) ||
extractNestedValue(value, [
["trustedPublishing"],
["trusted_publishing"],
["isTrustedPublishing"],
["verifiedPublisher"],
["oidc"],
["predicateType"],
]),
);
}
/**
* Extract advanced npm provenance and publishing properties from registry metadata.
*
* @param {object} packument npm packument body
* @param {string | undefined} version package version
* @returns {object[]} custom properties
*/
export function collectNpmRegistryProvenanceProperties(packument, version) {
const properties = [];
const versionBody = version ? packument?.versions?.[version] : undefined;
const publishTime = version ? packument?.time?.[version] : undefined;
const versionPublishTimes = Object.entries(packument?.time || {})
.filter(
([entryName, entryValue]) =>
!["created", "modified"].includes(entryName) &&
typeof entryValue === "string" &&
parseTimestamp(entryValue) !== undefined,
)
.map(([entryName, entryValue]) => ({
timestamp: parseTimestamp(entryValue),
version: entryName,
}));
const currentPublishTimestamp = parseTimestamp(publishTime);
const priorReleaseEntry = sortReleaseEntries(
versionPublishTimes.filter(
(entry) =>
entry.version !== version &&
currentPublishTimestamp !== undefined &&
entry.timestamp < currentPublishTimestamp,
),
).pop();
const priorVersionBody = priorReleaseEntry
? packument?.versions?.[priorReleaseEntry.version]
: undefined;
const currentMaintainerSet = uniqueIdentities([
...extractMaintainerIdentities(versionBody?.maintainers),
versionBody?._npmUser?.name,
versionBody?._npmUser?.email,
versionBody?.publisher?.name,
versionBody?.publisher?.email,
]);
const priorMaintainerSet = uniqueIdentities([
...extractMaintainerIdentities(priorVersionBody?.maintainers),
priorVersionBody?._npmUser?.name,
priorVersionBody?._npmUser?.email,
priorVersionBody?.publisher?.name,
priorVersionBody?.publisher?.email,
]);
const maintainerSetDrift = isDisjointIdentitySet(
currentMaintainerSet,
priorMaintainerSet,
);
const gapMetrics = releaseGapMetrics(versionPublishTimes, version);
const overlapMetrics = identityOverlapMetrics(
currentMaintainerSet,
priorMaintainerSet,
);
const cadenceMetrics = compressedCadenceMetrics(gapMetrics);
const publisherName =
versionBody?._npmUser?.name ||
versionBody?.publisher?.name ||
packument?.maintainers?.[0]?.name;
const publisherEmail =
versionBody?._npmUser?.email ||
versionBody?.publisher?.email ||
packument?.maintainers?.[0]?.email;
const provenanceCandidate =
versionBody?.dist?.provenance ||
versionBody?.provenance ||
versionBody?.dist?.attestations ||
versionBody?.attestations;
const provenanceUrl = normalizeProvenanceUrl(provenanceCandidate);
const provenanceDigests = collectProvenanceDigests(provenanceCandidate);
const provenanceKeyIds = collectProvenanceKeyIds(provenanceCandidate);
const provenanceSignatures = collectProvenanceSignatures(provenanceCandidate);
const provenancePredicateTypes =
collectProvenancePredicateTypes(provenanceCandidate);
const priorPublisherName =
priorVersionBody?._npmUser?.name || priorVersionBody?.publisher?.name;
const publisherDrift =
publisherName &&
priorPublisherName &&
publisherName.trim().toLowerCase() !==
priorPublisherName.trim().toLowerCase();
appendProperty(
properties,
"cdx:npm:packageCreatedTime",
packument?.time?.created,
);
appendProperty(
properties,
"cdx:npm:lastModifiedTime",
packument?.time?.modified,
);
appendProperty(properties, "cdx:npm:publishTime", publishTime);
appendProperty(properties, "cdx:npm:publisher", publisherName);
appendProperty(properties, "cdx:npm:publisherEmail", publisherEmail);
appendProperty(
properties,
"cdx:npm:maintainerSet",
currentMaintainerSet.join(", "),
);
appendProperty(
properties,
"cdx:npm:priorMaintainerSet",
priorMaintainerSet.join(", "),
);
appendProperty(
properties,
"cdx:npm:maintainerSetCount",
currentMaintainerSet.length,
);
appendProperty(
properties,
"cdx:npm:priorMaintainerSetCount",
priorMaintainerSet.length,
);
appendProperty(
properties,
"cdx:npm:maintainerOverlapCount",
overlapMetrics.overlapCount,
);
appendProperty(
properties,
"cdx:npm:maintainerOverlapRatio",
overlapMetrics.overlapRatio?.toFixed(2),
);
if (maintainerSetDrift) {
appendProperty(properties, "cdx:npm:maintainerSetDrift", "true");
}
if (overlapMetrics.partialDrift) {
appendProperty(properties, "cdx:npm:maintainerSetPartialDrift", "true");
}
appendProperty(
properties,
"cdx:npm:versionCount",
versionPublishTimes.length,
);
appendProperty(
properties,
"cdx:npm:releaseGapDays",
gapMetrics.currentGapDays?.toFixed(2),
);
appendProperty(
properties,
"cdx:npm:releaseGapBaselineDays",
gapMetrics.baselineDays?.toFixed(2),
);
appendProperty(
properties,
"cdx:npm:releaseGapSampleSize",
gapMetrics.sampleSize,
);
appendProperty(
properties,
"cdx:npm:releaseCadenceCompressionRatio",
cadenceMetrics.compressionRatio?.toFixed(2),
);
appendProperty(
properties,
"cdx:npm:priorVersion",
priorReleaseEntry?.version,
);
appendProperty(
properties,
"cdx:npm:priorPublishTime",
priorReleaseEntry
? packument?.time?.[priorReleaseEntry.version]
: undefined,
);
appendProperty(properties, "cdx:npm:priorPublisher", priorPublisherName);
if (publisherDrift) {
appendProperty(properties, "cdx:npm:publisherDrift", "true");
}
if (cadenceMetrics.compressedCadence) {
appendProperty(properties, "cdx:npm:compressedCadence", "true");
}
if (hasTrustedPublishingEvidence(provenanceCandidate)) {
appendProperty(properties, "cdx:npm:trustedPublishing", "true");
}
appendProperty(
properties,
"cdx:npm:artifactIntegrity",
versionBody?.dist?.integrity,
);
appendProperty(
properties,
"cdx:npm:artifactShasum",
versionBody?.dist?.shasum,
);
appendProperty(properties, "cdx:npm:provenanceUrl", provenanceUrl);
appendJoinedProperty(
properties,
"cdx:npm:provenanceDigest",
provenanceDigests,
);
appendJoinedProperty(properties, "cdx:npm:provenanceKeyId", provenanceKeyIds);
appendJoinedProperty(
properties,
"cdx:npm:provenanceSignature",
provenanceSignatures,
);
appendJoinedProperty(
properties,
"cdx:npm:provenancePredicateType",
provenancePredicateTypes,
);
return properties;
}
/**
* Extract advanced PyPI provenance and publishing properties from registry metadata.
*
* @param {object} projectBody PyPI JSON body
* @param {string | undefined} version package version
* @returns {object[]} custom properties
*/
export function collectPypiRegistryProvenanceProperties(projectBody, version) {
const properties = [];
const releaseEntries = [];
for (const [releaseVersion, releaseFilesForVersion] of Object.entries(
projectBody?.releases || {},
)) {
if (
!Array.isArray(releaseFilesForVersion) ||
!releaseFilesForVersion.length
) {
continue;
}
const releaseUploadTimes = uniqueStrings(
releaseFilesForVersion.map(
(file) =>
file?.upload_time_iso_8601 || file?.upload_time || file?.uploadTime,
),
);
const earliestUploadTime = releaseUploadTimes
.map((uploadTime) => ({
raw: uploadTime,
timestamp: parseTimestamp(uploadTime),
}))
.filter((entry) => entry.timestamp !== undefined)
.sort((left, right) => left.timestamp - right.timestamp)[0];
if (!earliestUploadTime) {
continue;
}
releaseEntries.push({
publishers: uniqueStrings(
releaseFilesForVersion.map(
(file) => file?.uploader || file?.uploaded_by,
),
),
timestamp: earliestUploadTime.timestamp,
uploadTime: earliestUploadTime.raw,
version: releaseVersion,
});
}
const releaseFiles = Array.isArray(projectBody?.releases?.[version])
? projectBody.releases[version]
: Array.isArray(projectBody?.urls)
? projectBody.urls
: [];
const uploadTimes = uniqueStrings(
releaseFiles.map(
(file) =>
file?.upload_time_iso_8601 || file?.upload_time || file?.uploadTime,
),
);
const uploaders = uniqueStrings(
releaseFiles.map((file) => file?.uploader || file?.uploaded_by),
);
const provenanceUrls = uniqueStrings(
releaseFiles.map(
(file) =>
normalizeProvenanceUrl(file?.provenance) ||
normalizeProvenanceUrl(file?.attestations) ||
normalizeProvenanceUrl(file?.provenance_url) ||
normalizeProvenanceUrl(file?.attestation_url),
),
);
const artifactDigestSha256 = uniqueStrings(
releaseFiles.map((file) => file?.digests?.sha256 || file?.sha256_digest),
);
const artifactDigestBlake2b256 = uniqueStrings(
releaseFiles.map((file) => file?.digests?.blake2b_256 || file?.blake2b_256),
);
const artifactDigestMd5 = uniqueStrings(
releaseFiles.map((file) => file?.digests?.md5 || file?.md5_digest),
);
const provenanceDigests = uniqueStrings(
releaseFiles.flatMap((file) =>
collectProvenanceDigests(
file?.provenance ||
file?.attestations ||
file?.provenance_url ||
file?.attestation_url,
),
),
);
const provenanceKeyIds = uniqueStrings(
releaseFiles.flatMap((file) =>
collectProvenanceKeyIds(file?.provenance || file?.attestations),
),
);
const provenanceSignatures = uniqueStrings(
releaseFiles.flatMap((file) =>
collectProvenanceSignatures(file?.provenance || file?.attestations),
),
);
const provenancePredicateTypes = uniqueStrings(
releaseFiles.flatMap((file) =>
collectProvenancePredicateTypes(file?.provenance || file?.attestations),
),
);
const trustedPublishing = releaseFiles.some((file) =>
hasTrustedPublishingEvidence(
file?.provenance ||
file?.attestations ||
file?.trusted_publishing ||
file?.uploaded_via ||
file?.uploaded_using ||
file?.provenance_url,
),
);
const uploaderVerified = releaseFiles.some(
(file) =>
file?.uploader_verified === true || file?.uploaderVerified === true,
);
const currentPublishTimestamp = parseTimestamp(uploadTimes[0]);
const priorReleaseEntry = sortReleaseEntries(
releaseEntries.filter(
(entry) =>
entry.version !== version &&
currentPublishTimestamp !== undefined &&
entry.timestamp < currentPublishTimestamp,
),
).pop();
const currentUploaders = uniqueStrings(uploaders);
const currentUploaderSet = uniqueIdentities(currentUploaders);
const priorUploaderSet = uniqueIdentities(
priorReleaseEntry?.publishers || [],
);
const uploaderSetDrift = isDisjointIdentitySet(
currentUploaderSet,
priorUploaderSet,
);
const gapMetrics = releaseGapMetrics(releaseEntries, version);
const overlapMetrics = identityOverlapMetrics(
currentUploaderSet,
priorUploaderSet,
);
const cadenceMetrics = compressedCadenceMetrics(gapMetrics);
const publisherDrift =
currentUploaders.length > 0 &&
priorReleaseEntry?.publishers?.length > 0 &&
currentUploaders.every(
(currentUploader) =>
!priorReleaseEntry.publishers.some(
(previousUploader) =>
previousUploader.toLowerCase() === currentUploader.toLowerCase(),
),
);
appendProperty(
properties,
"cdx:pypi:packageCreatedTime",
sortReleaseEntries([...releaseEntries])[0]?.uploadTime,
);
appendProperty(properties, "cdx:pypi:publishTime", uploadTimes[0]);
appendProperty(properties, "cdx:pypi:versionCount", releaseEntries.length);
appendProperty(properties, "cdx:pypi:publisher", uploaders.join(", "));
appendProperty(
properties,
"cdx:pypi:uploaderSet",
currentUploaderSet.join(", "),
);
appendProperty(
properties,
"cdx:pypi:priorUploaderSet",
priorUploaderSet.join(", "),
);
appendProperty(
properties,
"cdx:pypi:uploaderSetCount",
currentUploaderSet.length,
);
appendProperty(
properties,
"cdx:pypi:priorUploaderSetCount",
priorUploaderSet.length,
);
appendProperty(
properties,
"cdx:pypi:uploaderOverlapCount",
overlapMetrics.overlapCount,
);
appendProperty(
properties,
"cdx:pypi:uploaderOverlapRatio",
overlapMetrics.overlapRatio?.toFixed(2),
);
if (uploaderSetDrift) {
appendProperty(properties, "cdx:pypi:uploaderSetDrift", "true");
}
if (overlapMetrics.partialDrift) {
appendProperty(properties, "cdx:pypi:uploaderSetPartialDrift", "true");
}
appendProperty(
properties,
"cdx:pypi:releaseGapDays",
gapMetrics.currentGapDays?.toFixed(2),
);
appendProperty(
properties,
"cdx:pypi:releaseGapBaselineDays",
gapMetrics.baselineDays?.toFixed(2),
);
appendProperty(
properties,
"cdx:pypi:releaseGapSampleSize",
gapMetrics.sampleSize,
);
appendProperty(
properties,
"cdx:pypi:releaseCadenceCompressionRatio",
cadenceMetrics.compressionRatio?.toFixed(2),
);
appendProperty(
properties,
"cdx:pypi:priorVersion",
priorReleaseEntry?.version,
);
appendProperty(
properties,
"cdx:pypi:priorPublishTime",
priorReleaseEntry?.uploadTime,
);
appendProperty(
properties,
"cdx:pypi:priorPublisher",
priorReleaseEntry?.publishers?.join(", "),
);
if (publisherDrift) {
appendProperty(properties, "cdx:pypi:publisherDrift", "true");
}
if (cadenceMetrics.compressedCadence) {
appendProperty(properties, "cdx:pypi:compressedCadence", "true");
}
if (uploaderVerified) {
appendProperty(properties, "cdx:pypi:uploaderVerified", "true");
}
if (trustedPublishing) {
appendProperty(properties, "cdx:pypi:trustedPublishing", "true");
}
appendJoinedProperty(
properties,
"cdx:pypi:artifactDigestSha256",
artifactDigestSha256,
);
appendJoinedProperty(
properties,
"cdx:pypi:artifactDigestBlake2b256",
artifactDigestBlake2b256,
);
appendJoinedProperty(
properties,
"cdx:pypi:artifactDigestMd5",
artifactDigestMd5,
);
appendProperty(properties, "cdx:pypi:provenanceUrl", provenanceUrls[0]);
appendJoinedProperty(
properties,
"cdx:pypi:provenanceDigest",
provenanceDigests,
);
appendJoinedProperty(
properties,
"cdx:pypi:provenanceKeyId",
provenanceKeyIds,
);
appendJoinedProperty(
properties,
"cdx:pypi:provenanceSignature",
provenanceSignatures,
);
appendJoinedProperty(
properties,
"cdx:pypi:provenancePredicateType",
provenancePredicateTypes,
);
return properties;
}
/**
* Extract Cargo/crates.io release, publisher, and provenance-adjacent properties.
*
* @param {object} crateBody crates.io `/api/v1/crates/{name}` response body
* @param {string | undefined} version crate version
* @param {object} [ownersBody] crates.io `/api/v1/crates/{name}/owners` response body
* @returns {object[]} custom properties
*/
export function collectCargoRegistryProvenanceProperties(
crateBody,
version,
ownersBody,
) {
const properties = [];
const versions = Array.isArray(crateBody?.versions) ? crateBody.versions : [];
const currentVersionBody =
versions.find((entry) => entry?.num === version) || versions[0];
if (!currentVersionBody) {
return properties;
}
const releaseEntries = versions
.map((entry) => ({
publishers: uniqueStrings([
entry?.published_by?.login,
entry?.published_by?.name,
]),
timestamp: parseTimestamp(entry?.created_at || entry?.updated_at),
version: entry?.num,
rawTime: entry?.created_at || entry?.updated_at,
}))
.filter((entry) => entry.version && entry.timestamp !== undefined);
const currentPublishTimestamp = parseTimestamp(
currentVersionBody?.created_at || currentVersionBody?.updated_at,
);
const priorReleaseEntry = sortReleaseEntries(
releaseEntries.filter(
(entry) =>
entry.version !== currentVersionBody?.num &&
currentPublishTimestamp !== undefined &&
entry.timestamp < currentPublishTimestamp,
),
).pop();
const gapMetrics = releaseGapMetrics(releaseEntries, currentVersionBody?.num);
const cadenceMetrics = compressedCadenceMetrics(gapMetrics);
const currentPublisherSet = uniqueIdentities([
currentVersionBody?.published_by?.login,
currentVersionBody?.published_by?.name,
]);
const priorPublisherSet = uniqueIdentities(
priorReleaseEntry?.publishers || [],
);
const overlapMetrics = identityOverlapMetrics(
currentPublisherSet,
priorPublisherSet,
);
const publisherDrift = isDisjointIdentitySet(
currentPublisherSet,
priorPublisherSet,
);
const ownerSet = uniqueIdentities(
(ownersBody?.users || []).flatMap((owner) => [owner?.login, owner?.name]),
);
const trustpubCandidate =
currentVersionBody?.trustpub_data ||
currentVersionBody?.trustpubData ||
crateBody?.crate?.trustpub_data ||
crateBody?.crate?.trustpubData ||
crateBody?.versions?.find((entry) => entry?.trustpub_data)?.trustpub_data;
const provenanceUrl = normalizeProvenanceUrl(trustpubCandidate);
const provenanceDigests = collectProvenanceDigests(trustpubCandidate);
const provenanceKeyIds = collectProvenanceKeyIds(trustpubCandidate);
const provenanceSignatures = collectProvenanceSignatures(trustpubCandidate);
const provenancePredicateTypes =
collectProvenancePredicateTypes(trustpubCandidate);
appendProperty(
properties,
"cdx:cargo:packageCreatedTime",
sortReleaseEntries([...releaseEntries])[0]?.rawTime,
);
appendProperty(
properties,
"cdx:cargo:publishTime",
currentVersionBody?.created_at || currentVersionBody?.updated_at,
);
appendProperty(properties, "cdx:cargo:versionCount", releaseEntries.length);
appendProperty(
properties,
"cdx:cargo:publisher",
currentVersionBody?.published_by?.login ||
currentVersionBody?.published_by?.name,
);
appendProperty(
properties,
"cdx:cargo:priorPublisher",
priorReleaseEntry?.publishers?.join(", "),
);
appendProperty(
properties,
"cdx:cargo:publisherSet",
currentPublisherSet.join(", "),
);
appendProperty(
properties,
"cdx:cargo:publisherSetCount",
currentPublisherSet.length,
);
appendProperty(properties, "cdx:cargo:ownerSet", ownerSet.join(", "));
appendProperty(properties, "cdx:cargo:ownerSetCount", ownerSet.length);
appendProperty(
properties,
"cdx:cargo:publisherOverlapCount",
overlapMetrics.overlapCount,
);
appendProperty(
properties,
"cdx:cargo:publisherOverlapRatio",
overlapMetrics.overlapRatio?.toFixed(2),
);
appendProperty(
properties,
"cdx:cargo:priorVersion",
priorReleaseEntry?.version,
);
appendProperty(
properties,
"cdx:cargo:priorPublishTime",
priorReleaseEntry?.rawTime,
);
appendProperty(
properties,
"cdx:cargo:releaseGapDays",
gapMetrics.currentGapDays?.toFixed(2),
);
appendProperty(
properties,
"cdx:cargo:releaseGapBaselineDays",
gapMetrics.baselineDays?.toFixed(2),
);
appendProperty(
properties,
"cdx:cargo:releaseGapSampleSize",
gapMetrics.sampleSize,
);
appendProperty(
properties,
"cdx:cargo:releaseCadenceCompressionRatio",
cadenceMetrics.compressionRatio?.toFixed(2),
);
appendProperty(
properties,
"cdx:cargo:artifactDigestSha256",
currentVersionBody?.checksum,
);
appendProperty(properties, "cdx:cargo:edition", currentVersionBody?.edition);
appendProperty(properties, "cdx:cargo:hasLib", currentVersionBody?.has_lib);
appendJoinedProperty(
properties,
"cdx:cargo:binNames",
Array.isArray(currentVersionBody?.bin_names)
? currentVersionBody.bin_names
: [],
);
appendProperty(
properties,
"cdx:cargo:crateSize",
currentVersionBody?.crate_size,
);
if (currentVersionBody?.yanked === true) {
appendProperty(properties, "cdx:cargo:yanked", "true");
}
if (publisherDrift) {
appendProperty(properties, "cdx:cargo:publisherDrift", "true");
}
if (overlapMetrics.partialDrift) {
appendProperty(properties, "cdx:cargo:publisherSetPartialDrift", "true");
}
if (cadenceMetrics.compressedCadence) {
appendProperty(properties, "cdx:cargo:compressedCadence", "true");
}
if (
crateBody?.crate?.trustpub_only === true ||
hasTrustedPublishingEvidence(trustpubCandidate)
) {
appendProperty(properties, "cdx:cargo:trustedPublishing", "true");
}
appendProperty(properties, "cdx:cargo:provenanceUrl", provenanceUrl);
appendJoinedProperty(
properties,
"cdx:cargo:provenanceDigest",
provenanceDigests,
);
appendJoinedProperty(
properties,
"cdx:cargo:provenanceKeyId",
provenanceKeyIds,
);
appendJoinedProperty(
properties,
"cdx:cargo:provenanceSignature",
provenanceSignatures,
);
appendJoinedProperty(
properties,
"cdx:cargo:provenancePredicateType",
provenancePredicateTypes,
);
return properties;
}