@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
282 lines (266 loc) • 6.67 kB
JavaScript
import { basename } from "node:path";
const PYLOCK_FILE_REGEX = /^pylock(\.[^.]+)?\.toml$/;
const DEFAULT_PYPI_REGISTRIES = new Set([
"https://pypi.org/simple",
"https://pypi.org/simple/",
]);
const PYLOCK_TOP_LEVEL_KEYS = [
"lock-version",
"environments",
"requires-python",
"extras",
"dependency-groups",
"default-groups",
"created-by",
"tool",
];
const PYLOCK_PACKAGE_CUSTOM_KEYS = [
"marker",
"index",
"dependencies",
"extras",
"dependency-groups",
"attestation-identities",
"tool",
"vcs",
"directory",
"archive",
"sdist",
"wheels",
];
/**
* Check whether a file name conforms to pylock naming.
*
* @param {string} lockFilePath lock file path
* @returns {boolean} true if this is a pylock file
*/
export function isPyLockFile(lockFilePath) {
if (!lockFilePath) {
return false;
}
return PYLOCK_FILE_REGEX.test(basename(lockFilePath));
}
/**
* Check whether a parsed toml object follows pylock format.
*
* @param {object} lockTomlObj parsed toml object
* @returns {boolean} true if object appears to be pylock data
*/
export function isPyLockObject(lockTomlObj) {
return !!(
lockTomlObj?.["lock-version"] && Array.isArray(lockTomlObj.packages)
);
}
/**
* Get package entries from py lock data in a format-agnostic way.
*
* @param {object} lockTomlObj parsed toml object
* @returns {Array<object>} package entries
*/
export function getPyLockPackages(lockTomlObj) {
if (Array.isArray(lockTomlObj?.package)) {
return lockTomlObj.package;
}
if (Array.isArray(lockTomlObj?.packages)) {
return lockTomlObj.packages;
}
return [];
}
/**
* Convert top-level pylock keys to custom cdx properties.
*
* @param {object} lockTomlObj parsed toml object
* @returns {Array<object>} custom properties
*/
export function collectPyLockTopLevelProperties(lockTomlObj) {
const properties = [];
for (const akey of PYLOCK_TOP_LEVEL_KEYS) {
if (lockTomlObj?.[akey] !== undefined) {
properties.push({
name: `cdx:pylock:${akey.replaceAll("-", "_")}`,
value: toPropertyValue(lockTomlObj[akey]),
});
}
}
return properties;
}
/**
* Convert package-level pylock keys to custom cdx properties.
*
* @param {object} pkg pylock package entry
* @returns {Array<object>} custom properties
*/
export function collectPyLockPackageProperties(pkg) {
const properties = [];
for (const akey of PYLOCK_PACKAGE_CUSTOM_KEYS) {
if (pkg?.[akey] !== undefined) {
properties.push({
name: `cdx:pylock:${akey.replaceAll("-", "_")}`,
value: toPropertyValue(pkg[akey]),
});
}
}
return properties;
}
/**
* Build file components from pylock source entries.
*
* @param {object} pkg pylock package entry
* @param {string} lockFile lock file path
* @returns {Array<object>} file components
*/
export function collectPyLockFileComponents(pkg, lockFile) {
const fileComponents = [];
if (pkg?.archive) {
const archiveComp = createArtifactComponent(
pkg.archive,
"archive",
lockFile,
pkg.name,
);
if (archiveComp) {
fileComponents.push(archiveComp);
}
}
if (pkg?.sdist) {
const sdistComp = createArtifactComponent(
pkg.sdist,
"sdist",
lockFile,
pkg.name,
);
if (sdistComp) {
fileComponents.push(sdistComp);
}
}
if (Array.isArray(pkg?.wheels)) {
for (const awheel of pkg.wheels) {
const wheelComp = createArtifactComponent(
awheel,
"wheel",
lockFile,
pkg.name,
);
if (wheelComp) {
fileComponents.push(wheelComp);
}
}
}
return fileComponents;
}
/**
* Check whether index points to the default pypi registry.
*
* @param {string} indexUrl index URL from pylock
* @returns {boolean} true for default pypi
*/
export function isDefaultPypiRegistry(indexUrl) {
if (!indexUrl) {
return false;
}
return DEFAULT_PYPI_REGISTRIES.has(indexUrl);
}
function createArtifactComponent(artifact, sourceType, lockFile, packageName) {
if (!artifact) {
return null;
}
const properties = [{ name: "SrcFile", value: lockFile }];
properties.push({
name: "cdx:pylock:file:source_type",
value: sourceType,
});
if (artifact.url) {
properties.push({
name: "cdx:pylock:file:url",
value: artifact.url,
});
}
if (artifact.path) {
properties.push({
name: "cdx:pylock:file:path",
value: artifact.path,
});
}
if (artifact.size !== undefined) {
properties.push({
name: "cdx:pylock:file:size",
value: `${artifact.size}`,
});
}
if (artifact["upload-time"]) {
properties.push({
name: "cdx:pylock:file:upload_time",
value: toPropertyValue(artifact["upload-time"]),
});
}
if (artifact.subdirectory) {
properties.push({
name: "cdx:pylock:file:subdirectory",
value: artifact.subdirectory,
});
}
return {
type: "file",
name: resolveArtifactName(artifact, packageName, sourceType),
hashes: toHashes(artifact.hashes),
properties,
};
}
function toHashes(hashesObj) {
if (!hashesObj || typeof hashesObj !== "object") {
return undefined;
}
const hashes = [];
for (const [alg, content] of Object.entries(hashesObj)) {
if (!content) {
continue;
}
const normalizedAlg = normalizeHashAlgorithm(alg);
hashes.push({ alg: normalizedAlg, content: `${content}` });
}
return hashes.length ? hashes : undefined;
}
function resolveArtifactName(artifact, packageName, sourceType) {
if (artifact.name) {
return artifact.name;
}
if (artifact.path) {
return basename(artifact.path);
}
if (artifact.url) {
try {
return basename(new URL(artifact.url).pathname);
} catch (_err) {
return `${packageName || "package"}-${sourceType}-invalid-url`;
}
}
return `${packageName || "package"}-${sourceType}`;
}
function normalizeHashAlgorithm(algorithm) {
const normalized = `${algorithm}`.toLowerCase();
if (normalized.startsWith("sha3")) {
return `SHA3-${normalized.slice(4).replace(/^[-_]/, "")}`;
}
if (normalized.startsWith("sha")) {
return `SHA-${normalized.slice(3).replace(/^[-_]/, "").toUpperCase()}`;
}
return normalized.toUpperCase();
}
function toPropertyValue(value) {
if (value === null) {
return "null";
}
if (value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return `${value}`;
}
if (value instanceof Date) {
return value.toISOString();
}
return JSON.stringify(value);
}