UNPKG

snyk-nodejs-lockfile-parser

Version:
148 lines 6.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hashLabelsFromIntegrity = hashLabelsFromIntegrity; exports.distributionUrlLabel = distributionUrlLabel; exports.hashLabelFromResolvedFragment = hashLabelFromResolvedFragment; exports.getComponentMetadataLabels = getComponentMetadataLabels; const debugModule = require("debug"); const debug = debugModule('snyk-nodejs-lockfile-parser:component-metadata'); // Maps a Subresource Integrity (SRI) algorithm token to its dep-graph label key. // Labels use the `hash:<algorithm>` convention with hyphenated algorithm names // (`hash:sha-512`, `hash:sha-256`, ...) and lowercase-hex values. Hex matches the CycloneDX // hash `content` format, so downstream SBOM generation can populate CycloneDX component // hashes / SPDX package checksums directly. SRI base64 values are decoded to lowercase hex // below. const SRI_ALG_TO_LABEL = { md5: 'hash:md5', sha1: 'hash:sha-1', sha256: 'hash:sha-256', sha384: 'hash:sha-384', sha512: 'hash:sha-512', }; // Expected raw digest length (bytes) per algorithm. `Buffer.from(x, 'base64')` is lenient and // will not throw on a corrupt/truncated value, so we reject anything whose decoded length does // not match — otherwise we would emit a hex string that violates the CycloneDX `content` regex. const SRI_ALG_BYTES = { md5: 16, sha1: 20, sha256: 32, sha384: 48, sha512: 64, }; /** * Convert a lockfile `integrity` (SRI) string into `hash:<algorithm>` labels with * lowercase-hex values. Returns an empty object (and debug-logs) when the value is absent * or unparseable — never throws. */ function hashLabelsFromIntegrity(integrity, nodeId) { if (!integrity) { debug(`No integrity for ${nodeId}; skipping hash labels`); return {}; } const labels = {}; // SRI may contain multiple whitespace-separated hashes, each "<alg>-<base64>[?opts]". for (const token of integrity.trim().split(/\s+/)) { const dashIdx = token.indexOf('-'); if (dashIdx === -1) { debug(`Unrecognised integrity "${token}" for ${nodeId}; skipping`); continue; } const alg = token.slice(0, dashIdx).toLowerCase(); const base64 = token.slice(dashIdx + 1).split('?')[0]; // strip optional ?opts const key = SRI_ALG_TO_LABEL[alg]; if (!key || !base64) { debug(`Unrecognised integrity "${token}" for ${nodeId}; skipping`); continue; } const digest = Buffer.from(base64, 'base64'); if (digest.length !== SRI_ALG_BYTES[alg]) { debug(`Integrity "${token}" for ${nodeId} decoded to ${digest.length} bytes, ` + `expected ${SRI_ALG_BYTES[alg]} for ${alg}; skipping`); continue; } labels[key] = digest.toString('hex'); } return labels; } /** * Produce a `distribution:url` label from a lockfile `resolved` value when it is an * http(s) tarball URL. `resolved` can also be a `file:`/workspace path for local deps; * those are debug-logged and skipped. */ function distributionUrlLabel(resolved, nodeId) { if (!resolved) { debug(`No resolved URL for ${nodeId}; skipping distribution:url label`); return {}; } let url; try { url = new URL(resolved); } catch { debug(`resolved "${resolved}" for ${nodeId} is not a parseable URL; skipping distribution:url label`); return {}; } // URL normalises the scheme to lowercase, so this also accepts HTTPS://, Http://, etc. if (url.protocol !== 'http:' && url.protocol !== 'https:') { debug(`resolved "${resolved}" for ${nodeId} is not an http(s) URL; skipping distribution:url label`); return {}; } // Strip any embedded basic-auth userinfo (scheme://user:pass@host) so private-registry // credentials never leak into the emitted label or downstream SBOM externalReferences. if (url.username || url.password) { url.password = ''; url.username = ''; debug(`Stripped credentials from resolved URL for ${nodeId}`); } // yarn v1 appends the tarball shasum as a URL fragment (`…-1.0.0.tgz#<sha1>`); npm does not. // Drop it so distribution:url is a clean tarball URL — the shasum is surfaced separately as a // hash:sha-1 label (see hashLabelFromResolvedFragment). if (url.hash) { url.hash = ''; } return { 'distribution:url': url.toString() }; } /** * yarn v1 `resolved` values embed the npm-registry tarball shasum as a URL fragment * (`…-1.0.0.tgz#<40-hex-sha1>`). For older lockfiles that carry no `integrity` line this is the * only available hash, so surface it as `hash:sha-1`. Restricted to http(s) tarball URLs: a * `#<40-hex>` fragment on a `git:`/`git+ssh:` resolution is a commit SHA, not a package hash. */ function hashLabelFromResolvedFragment(resolved, nodeId) { if (!resolved) { return {}; } let url; try { url = new URL(resolved); } catch { return {}; } if (url.protocol !== 'http:' && url.protocol !== 'https:') { // git/file/etc. — a fragment here is a commit-ish, not a package shasum. return {}; } const fragment = url.hash.replace(/^#/, ''); if (!/^[0-9a-f]{40}$/i.test(fragment)) { if (fragment) { debug(`resolved fragment "${fragment}" for ${nodeId} is not a sha1 shasum; skipping`); } return {}; } return { 'hash:sha-1': fragment.toLowerCase() }; } /** * Build the full set of component-metadata labels (package hashes + distribution URL) for a * node, sourced from its lockfile `integrity` / `resolved` values. */ function getComponentMetadataLabels(node) { return { ...distributionUrlLabel(node.resolved, node.id), // Fragment-derived sha1 first so an explicit `integrity` sha1 wins on overlap (they describe // the same digest for npm-registry tarballs). ...hashLabelFromResolvedFragment(node.resolved, node.id), ...hashLabelsFromIntegrity(node.integrity, node.id), }; } //# sourceMappingURL=component-metadata-labels.js.map