snyk-docker-plugin
Version:
Snyk CLI docker plugin
228 lines • 9.66 kB
JavaScript
;
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