UNPKG

snyk-docker-plugin

Version:
228 lines 9.66 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getApkPackagesFromResults = exports.resolveApkOwnership = exports.resolveOwnerForEvidencePath = exports.buildApkPathIndex = exports.toSymlinkGraph = exports.isChainguardDistro = void 0; const types_1 = require("../types"); const path_canonicalization_1 = require("./path-canonicalization"); const CHAINGUARD_DISTROS = new Set(["wolfi", "chainguard"]); function isChainguardDistro(osRelease) { return !!osRelease && CHAINGUARD_DISTROS.has(osRelease.name.toLowerCase()); } exports.isChainguardDistro = isChainguardDistro; function toSymlinkGraph(symlinks) { const graph = new Map(); if (!symlinks) { return graph; } for (const [symlinkPath, target] of Object.entries(symlinks)) { graph.set((0, path_canonicalization_1.normalizeAbsolutePath)(symlinkPath), target); } return graph; } exports.toSymlinkGraph = toSymlinkGraph; function buildApkPathIndex(packages, symlinkGraph) { var _a, _b; const exactFileOwners = new Map(); const directoryTrie = { owners: [], children: new Map() }; for (const pkg of packages) { for (const filePath of (_a = pkg.Files) !== null && _a !== void 0 ? _a : []) { const canonical = (0, path_canonicalization_1.canonicalizePath)(filePath, symlinkGraph); addToOwnerMap(exactFileOwners, canonical, pkg); } for (const dirPath of (_b = pkg.Directories) !== null && _b !== void 0 ? _b : []) { const canonical = (0, path_canonicalization_1.canonicalizePath)(dirPath, symlinkGraph); insertDirectoryOwner(directoryTrie, canonical, pkg); } } return { exactFileOwners, directoryTrie }; } exports.buildApkPathIndex = buildApkPathIndex; function resolveOwnerForEvidencePath(evidencePath, index, symlinkGraph) { const canonical = (0, path_canonicalization_1.canonicalizePath)(evidencePath, symlinkGraph); const exactOwners = index.exactFileOwners.get(canonical); if (exactOwners && exactOwners.length > 0) { return { owner: pickExactOwner(exactOwners), matchKind: "exact", prefixLength: canonical.split("/").filter(Boolean).length, }; } return resolveDirectoryOwner(canonical, index); } exports.resolveOwnerForEvidencePath = resolveOwnerForEvidencePath; function resolveDirectoryOwner(canonicalPath, index) { const segments = canonicalPath.split("/").filter(Boolean); let node = index.directoryTrie; let deepestOwners; let deepestPrefix = 0; for (let i = 0; i < segments.length; i++) { const child = node.children.get(segments[i]); if (!child) { break; } node = child; if (node.owners.length > 0) { deepestOwners = node.owners; deepestPrefix = i + 1; } } if (!deepestOwners) { return undefined; } // A directory declared by more than one distinct package is shared, so no // single package wholly owns its contents. Fail closed rather than guess. const owner = uniqueDeclaredOwner(deepestOwners); if (!owner) { return undefined; } return { owner, matchKind: "directory", prefixLength: deepestPrefix }; } function uniqueDeclaredOwner(owners) { const first = owners[0]; return owners.every((o) => ownerKey(o) === ownerKey(first)) ? first : undefined; } /** * Resolve APK package ownership for a set of candidates on Wolfi/Chainguard * images. Each candidate (a single npm package, or a whole Go/Java result) is * resolved independently: all of its evidence paths must be wholly owned by one * consistent APK package, or the candidate is omitted (fail closed per * candidate). Per Chainguard's scanner spec, a dependency with any unowned path * is not covered by advisory data and must keep its findings — we skip rather * than guess and risk suppressing real vulnerabilities in user-added software. * Candidates carrying a coordinate (name@version) are deduplicated, keeping an * owned occurrence over an unowned one. * https://github.com/chainguard-dev/vulnerability-scanner-support/blob/main/docs/scanning_implementation.md */ function resolveApkOwnership(candidates, index, symlinkGraph, osRelease) { var _a; if (!isChainguardDistro(osRelease) || candidates.length === 0) { return undefined; } const ownedPackages = []; // `seen` holds matched coordinates only, so an owned occurrence always wins // over an unowned one. It must NOT include unmatched coordinates, or a later // owned copy of the same coordinate would be skipped. const seen = new Set(); // Memoize per-path owner lookups so a path shared by several candidates (or an // unmatched coordinate seen repeatedly) is resolved only once. const resolvedByPath = new Map(); // Sort by first evidence path so coordinate dedup is deterministic regardless // of discovery order: when a coordinate is bundled under more than one apk // package, the lexicographically-first evidence path wins. Which origin is // "correct" in that case is a downstream (@snyk/vuln) question. const ordered = [...candidates].sort((a, b) => { var _a, _b; return ((_a = a.evidencePaths[0]) !== null && _a !== void 0 ? _a : "").localeCompare((_b = b.evidencePaths[0]) !== null && _b !== void 0 ? _b : ""); }); for (const candidate of ordered) { if (candidate.evidencePaths.length === 0) { continue; } const coordinate = candidate.name && candidate.version ? `${candidate.name}@${candidate.version}` : undefined; if (coordinate && seen.has(coordinate)) { continue; } // Fail closed per candidate: every evidence path must resolve to one // consistent owner, else the candidate keeps its findings. const matches = []; let allOwned = true; for (const evidencePath of candidate.evidencePaths) { const normalized = (0, path_canonicalization_1.normalizeAbsolutePath)(evidencePath); if (!resolvedByPath.has(normalized)) { resolvedByPath.set(normalized, resolveOwnerForEvidencePath(normalized, index, symlinkGraph)); } const match = resolvedByPath.get(normalized); if (!match) { allOwned = false; break; } matches.push(match); } if (!allOwned) { continue; } const owner = pickConsistentOwner(matches); if (!owner) { continue; } if (coordinate) { seen.add(coordinate); } ownedPackages.push({ evidencePaths: candidate.evidencePaths, apkPackageName: owner.Name, apkPackageVersion: owner.Version, originPackage: (_a = owner.Source) !== null && _a !== void 0 ? _a : owner.Name, ...(candidate.name ? { name: candidate.name } : {}), ...(candidate.version ? { version: candidate.version } : {}), }); } if (ownedPackages.length === 0) { return undefined; } return { distroId: osRelease.name, ownedPackages }; } exports.resolveApkOwnership = resolveApkOwnership; function ownerKey(pkg) { return `${pkg.Name}@${pkg.Version}`; } function pickConsistentOwner(matches) { var _a; return (_a = uniqueOwner(matches)) !== null && _a !== void 0 ? _a : pickBestOwnerAcrossPaths(matches); } /** * When evidence paths disagree on an owner, an owner backed by an exact file * match outranks owners only inferred from a parent directory; among * directory-only matches, the deepest prefix wins. Evidence that is still * split between owners yields no owner. */ function pickBestOwnerAcrossPaths(matches) { const exactMatches = matches.filter((m) => m.matchKind === "exact"); if (exactMatches.length > 0) { return uniqueOwner(exactMatches); } const deepest = Math.max(...matches.map((m) => m.prefixLength)); return uniqueOwner(matches.filter((m) => m.prefixLength === deepest)); } function uniqueOwner(matches) { const first = matches[0].owner; return matches.every((m) => ownerKey(m.owner) === ownerKey(first)) ? first : undefined; } function pickExactOwner(owners) { if (owners.length === 1) { return owners[0]; } const originMatch = owners.find((o) => o.Name === o.Source); return originMatch !== null && originMatch !== void 0 ? originMatch : owners[0]; } function addToOwnerMap(map, canonicalPath, pkg) { var _a; const existing = (_a = map.get(canonicalPath)) !== null && _a !== void 0 ? _a : []; existing.push(pkg); map.set(canonicalPath, existing); } function insertDirectoryOwner(root, dirPath, pkg) { const segments = dirPath.split("/").filter(Boolean); let node = root; for (const segment of segments) { let child = node.children.get(segment); if (!child) { child = { owners: [], children: new Map() }; node.children.set(segment, child); } node = child; } node.owners.push(pkg); } function getApkPackagesFromResults(results) { var _a; if (!results) { return []; } const apkResult = results.find((r) => r.AnalyzeType === types_1.AnalysisType.Apk); return (_a = apkResult === null || apkResult === void 0 ? void 0 : apkResult.Analysis) !== null && _a !== void 0 ? _a : []; } exports.getApkPackagesFromResults = getApkPackagesFromResults; //# sourceMappingURL=apk-ownership.js.map