snyk-poetry-lockfile-parser
Version:
Generate a dep graph given poetry.lock and pyproject.toml files
147 lines • 6.42 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getComponentMetadataLabels = getComponentMetadataLabels;
const debugLib = require("debug");
const debug = debugLib('snyk-poetry-lockfile-parser');
// poetry locks only ever carry sha256, but map defensively so an unexpected algorithm is
// skipped rather than mislabelled.
const HASH_ALG_TO_LABEL = {
sha256: 'hash:sha-256',
sha512: 'hash:sha-512',
sha384: 'hash:sha-384',
sha1: 'hash:sha-1',
md5: 'hash:md5',
};
function getComponentMetadataLabels(pkg) {
// No files recorded means no artifact to hash and no distribution to point at (real PyPI /
// index packages always list files; fileless entries are anomalous), so report nothing.
if (!pkg.files || pkg.files.length === 0) {
return {};
}
// Select the artifact once so the hash and the distribution URL describe the *same* file.
const selected = selectArtifact(pkg.name, pkg.files);
return {
...hashLabel(selected),
...distributionUrlLabel(pkg.name, pkg.source, selected),
};
}
function hashLabel(selected) {
if (!selected) {
return {};
}
return hashToLabel(selected.hash);
}
// Collapse the lockfile's candidate `files` set to a single artifact. The label channel
// holds one hash per node and the lockfile lists every published distribution (sdist + all
// wheels) platform-agnostically, so we choose deterministically.
function selectArtifact(name, files) {
if (!files || files.length === 0) {
return undefined;
}
// A single published file (or a `url`/`file` source pinned to one artifact) is
// unambiguous — use it regardless of platform-specificity.
if (files.length === 1) {
return files[0];
}
// 1. platform-independent wheel (`*-none-any.whl`) — what pip installs everywhere for a
// pure-python package, and deterministic across parsing hosts.
const universalWheel = files.find((f) => isUniversalWheel(f.file));
if (universalWheel) {
return universalWheel;
}
// 2. sdist (`.tar.gz` / `.zip`) — canonical, platform-independent release identity.
const sdist = files.find((f) => isSdist(f.file));
if (sdist) {
return sdist;
}
// 3. only platform-specific wheels, no universal wheel, no sdist: we cannot know the
// target platform offline, so emit no hash rather than guess.
debug(`No platform-independent artifact for "${name}"; emitting no hash (only platform-specific wheels found).`);
return undefined;
}
function isUniversalWheel(filename) {
return /-none-any\.whl$/i.test(filename);
}
function isSdist(filename) {
const lower = filename.toLowerCase();
return lower.endsWith('.tar.gz') || lower.endsWith('.zip');
}
function hashToLabel(hash) {
const separatorIndex = hash.indexOf(':');
if (separatorIndex === -1) {
return {};
}
const alg = hash.slice(0, separatorIndex).toLowerCase();
const value = hash.slice(separatorIndex + 1);
const label = HASH_ALG_TO_LABEL[alg];
if (!label || !value) {
return {};
}
return { [label]: value.toLowerCase() };
}
// poetry records a `[package.source]` stanza for every index except the built-in PyPI
// (including a custom primary/default source, which is recorded as `legacy`). So the absence
// of a source stanza reliably means the package came from pypi.org, and we can build its
// PEP 503 project page from this well-known root.
const PYPI_SIMPLE_ROOT = 'https://pypi.org/simple';
function distributionUrlLabel(name, source, selected) {
// No source stanza => built-in PyPI. Build its project page (see PYPI_SIMPLE_ROOT note).
if (!source) {
return projectPageLabel(PYPI_SIMPLE_ROOT, name, selected);
}
// Direct-URL dependency: the source URL is the exact artifact.
if (source.type === 'url' && source.url) {
const cleaned = sanitizeUrl(source.url);
return cleaned ? { 'distribution:url': cleaned } : {};
}
// Private index (PEP 503 Simple API repository): poetry recorded the real index root.
if (source.type === 'legacy' && source.url) {
return projectPageLabel(source.url, name, selected);
}
// git / file / directory: not a downloadable distribution artifact.
return {};
}
// Build the PEP 503 project-page URL for `name` under `root`, pointed at the selected file:
// <root>/<pep503-normalized-name>/#<selected-filename>. The `#<filename>` is an identifier
// fragment (which file the sibling hash label describes), NOT a navigable anchor or a
// download URL — the page's real anchors link to the artifacts, which we cannot reconstruct
// offline. When no artifact was selected we emit the bare package-granular project page.
function projectPageLabel(root, name, selected) {
const projectPage = buildSimpleIndexProjectUrl(root, name, selected);
return projectPage ? { 'distribution:url': projectPage } : {};
}
function buildSimpleIndexProjectUrl(root, name, selected) {
const cleaned = sanitizeUrl(root);
if (!cleaned) {
return undefined;
}
const base = cleaned.replace(/\/+$/, '');
const projectPage = `${base}/${normalizePep503Name(name)}/`;
return selected ? `${projectPage}#${selected.file}` : projectPage;
}
// PEP 503: normalize by lowercasing and collapsing runs of -, _, . to a single -.
function normalizePep503Name(name) {
return name.toLowerCase().replace(/[-_.]+/g, '-');
}
// Reduce a source URL to its canonical identity before it becomes a
// distribution:url provenance label: clear basic-auth userinfo, the query
// string, and the fragment. distribution:url is not a fetch target — a PEP 503
// project page is `<root>/<pkg>/#<file>` and needs no query, and a direct-url
// artifact's query is not part of its identity — so the query never belongs in
// the label. Query strings can also carry authentication material, so dropping
// them keeps that out of the label. The fragment is re-added by the caller from
// the selected artifact, so a source-provided one is noise.
function sanitizeUrl(rawUrl) {
try {
const url = new URL(rawUrl);
url.username = '';
url.password = '';
url.search = '';
url.hash = '';
return url.toString();
}
catch (_a) {
return undefined;
}
}
//# sourceMappingURL=component-metadata-labels.js.map