snyk-docker-plugin
Version:
Snyk CLI docker plugin
482 lines • 20.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildResponse = void 0;
const dep_graph_1 = require("@snyk/dep-graph");
const evidence_paths_1 = require("./analyzer/applications/evidence-paths");
const apk_ownership_1 = require("./analyzer/package-managers/apk-ownership");
// Module that provides functions to collect and build response after all
// analyses' are done.
const dockerfile_1 = require("./dockerfile");
const provenance_parser_1 = require("./extractor/provenance-parser");
const scan_payload_metrics_1 = require("./scan-payload-metrics");
const utils_1 = require("./utils");
const version_1 = require("./version");
async function buildResponse(depsAnalysis, dockerfileAnalysis, excludeBaseImageVulns, names, ociDistributionMetadata, options) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const deps = depsAnalysis.depTree.dependencies;
// Expand dockerfile packages and auto detected user instructions if
// they are provided. These objects are mutated in place, ensuring the
// expanded packages are used in the subsequent steps and fact building.
if (dockerfileAnalysis === null || dockerfileAnalysis === void 0 ? void 0 : dockerfileAnalysis.dockerfilePackages) {
getUserInstructionDeps(dockerfileAnalysis.dockerfilePackages, deps);
}
if ((_a = depsAnalysis.autoDetectedUserInstructions) === null || _a === void 0 ? void 0 : _a.dockerfilePackages) {
getUserInstructionDeps(depsAnalysis.autoDetectedUserInstructions.dockerfilePackages, deps);
}
const dockerfilePkgs = (_b = dockerfileAnalysis === null || dockerfileAnalysis === void 0 ? void 0 : dockerfileAnalysis.dockerfilePackages) !== null && _b !== void 0 ? _b : (_c = depsAnalysis.autoDetectedUserInstructions) === null || _c === void 0 ? void 0 : _c.dockerfilePackages;
if (dockerfilePkgs) {
const finalDeps = excludeBaseImageDeps(deps, dockerfilePkgs, excludeBaseImageVulns);
annotateLayerIds(finalDeps, dockerfilePkgs);
depsAnalysis.depTree.dependencies = finalDeps;
}
// `dockerLayerDiffId` is the new layer-identity label introduced by the
// vulns-by-layer feature. It carries the diffID (`sha256:...`) of the
// rootfs layer that introduced each package
if ((_d = depsAnalysis.introducingLayerByPackage) === null || _d === void 0 ? void 0 : _d.size) {
annotateDockerLayerDiffIds(depsAnalysis.depTree.dependencies, depsAnalysis.introducingLayerByPackage);
}
/** This must be called after all final changes to the DependencyTree. */
const depGraph = await dep_graph_1.legacy.depTreeToGraph(depsAnalysis.depTree, depsAnalysis.packageFormat);
const additionalFacts = [];
const hashes = depsAnalysis.binaries;
if (hashes && hashes.length > 0) {
const keyBinariesHashesFact = {
type: "keyBinariesHashes",
data: hashes,
};
additionalFacts.push(keyBinariesHashesFact);
}
if (depsAnalysis.baseRuntimes && depsAnalysis.baseRuntimes.length > 0) {
const baseRuntimesFact = {
type: "baseRuntimes",
data: depsAnalysis.baseRuntimes,
};
additionalFacts.push(baseRuntimesFact);
}
if (dockerfileAnalysis !== undefined) {
const dockerfileAnalysisFact = {
type: "dockerfileAnalysis",
data: dockerfileAnalysis,
};
additionalFacts.push(dockerfileAnalysisFact);
}
if (depsAnalysis.imageId) {
const imageIdFact = {
type: "imageId",
data: depsAnalysis.imageId,
};
additionalFacts.push(imageIdFact);
}
if (depsAnalysis.imageLayers && depsAnalysis.imageLayers.length > 0) {
const imageLayersFact = {
type: "imageLayers",
data: depsAnalysis.imageLayers,
};
additionalFacts.push(imageLayersFact);
}
if (depsAnalysis.imageLabels) {
const imageLabels = {
type: "imageLabels",
data: depsAnalysis.imageLabels,
};
additionalFacts.push(imageLabels);
}
if (depsAnalysis.containerConfig) {
const containerConfigFact = {
type: "containerConfig",
data: {
...(depsAnalysis.containerConfig.User !== undefined && {
user: depsAnalysis.containerConfig.User,
}),
...(depsAnalysis.containerConfig.ExposedPorts !== undefined && {
exposedPorts: depsAnalysis.containerConfig.ExposedPorts
? Object.keys(depsAnalysis.containerConfig.ExposedPorts)
: null,
}),
...(depsAnalysis.containerConfig.Env !== undefined && {
env: depsAnalysis.containerConfig.Env,
}),
...(depsAnalysis.containerConfig.Entrypoint !== undefined && {
entrypoint: depsAnalysis.containerConfig.Entrypoint,
}),
...(depsAnalysis.containerConfig.Cmd !== undefined && {
cmd: depsAnalysis.containerConfig.Cmd,
}),
...(depsAnalysis.containerConfig.Volumes !== undefined && {
volumes: depsAnalysis.containerConfig.Volumes
? Object.keys(depsAnalysis.containerConfig.Volumes)
: null,
}),
...(depsAnalysis.containerConfig.WorkingDir !== undefined && {
workingDir: depsAnalysis.containerConfig.WorkingDir,
}),
...(depsAnalysis.containerConfig.StopSignal !== undefined && {
stopSignal: depsAnalysis.containerConfig.StopSignal,
}),
...(depsAnalysis.containerConfig.ArgsEscaped !== undefined && {
argsEscaped: depsAnalysis.containerConfig.ArgsEscaped,
}),
},
};
additionalFacts.push(containerConfigFact);
}
let historyFact;
if (depsAnalysis.history && depsAnalysis.history.length > 0) {
historyFact = {
type: "history",
data: depsAnalysis.history.map((entry) => ({
...(entry.created !== undefined && { created: entry.created }),
...(entry.author !== undefined && { author: entry.author }),
...(entry.created_by !== undefined && { createdBy: entry.created_by }),
...(entry.comment !== undefined && { comment: entry.comment }),
...(entry.empty_layer !== undefined && {
emptyLayer: entry.empty_layer,
}),
})),
};
additionalFacts.push(historyFact);
}
if (depsAnalysis.imageCreationTime) {
const imageCreationTimeFact = {
type: "imageCreationTime",
data: depsAnalysis.imageCreationTime,
};
additionalFacts.push(imageCreationTimeFact);
}
let rootFsFact;
if (depsAnalysis.rootFsLayers &&
Array.isArray(depsAnalysis.rootFsLayers) &&
depsAnalysis.rootFsLayers.length > 0) {
rootFsFact = {
type: "rootFs",
data: depsAnalysis.rootFsLayers,
};
additionalFacts.push(rootFsFact);
}
if (depsAnalysis.depTree.targetOS.prettyName) {
const imageOsReleasePrettyNameFact = {
type: "imageOsReleasePrettyName",
data: depsAnalysis.depTree.targetOS.prettyName,
};
additionalFacts.push(imageOsReleasePrettyNameFact);
}
const manifestFiles = depsAnalysis.manifestFiles.length > 0
? depsAnalysis.manifestFiles
: undefined;
if (manifestFiles) {
const imageManifestFilesFact = {
type: "imageManifestFiles",
data: manifestFiles,
};
additionalFacts.push(imageManifestFilesFact);
}
const autoDetectedPackages = (_e = depsAnalysis.autoDetectedUserInstructions) === null || _e === void 0 ? void 0 : _e.dockerfilePackages;
const autoDetectedLayers = (_f = depsAnalysis.autoDetectedUserInstructions) === null || _f === void 0 ? void 0 : _f.dockerfileLayers;
if (autoDetectedPackages &&
Object.keys(autoDetectedPackages).length > 0 &&
autoDetectedLayers &&
Object.keys(autoDetectedLayers).length > 0) {
const autoDetectedUserInstructionsFact = {
type: "autoDetectedUserInstructions",
data: {
dockerfileLayers: autoDetectedLayers,
dockerfilePackages: autoDetectedPackages,
},
};
additionalFacts.push(autoDetectedUserInstructionsFact);
}
// APK package ownership only applies to Chainguard/Wolfi images, so the
// image-global path index is built once here and only when it will be used;
// non-Chainguard images skip the build (and all resolution below) entirely.
const osRelease = depsAnalysis.osRelease;
const ownershipEnabled = (0, apk_ownership_1.isChainguardDistro)(osRelease);
const apkSymlinkGraph = (0, apk_ownership_1.toSymlinkGraph)(depsAnalysis.symlinks);
const apkPathIndex = ownershipEnabled
? (0, apk_ownership_1.buildApkPathIndex)((0, apk_ownership_1.getApkPackagesFromResults)(depsAnalysis.results), apkSymlinkGraph)
: undefined;
const applicationDependenciesScanResults = (depsAnalysis.applicationDependenciesScanResults || []).map((appDepsScanResult) => {
if (depsAnalysis.imageId) {
const imageIdFact = {
type: "imageId",
data: depsAnalysis.imageId,
};
appDepsScanResult.facts.push(imageIdFact);
}
if (names && names.length > 0) {
const imageNamesFact = {
type: "imageNames",
data: { names },
};
appDepsScanResult.facts.push(imageNamesFact);
}
if (ociDistributionMetadata) {
const metadataFact = {
type: "ociDistributionMetadata",
data: ociDistributionMetadata,
};
appDepsScanResult.facts.push(metadataFact);
}
const appPluginVersionFact = {
type: "pluginVersion",
data: version_1.PLUGIN_VERSION,
};
appDepsScanResult.facts.push(appPluginVersionFact);
// TODO(vulns-by-layer, app-scan milestone): re-enable when app-package
// layer attribution lands. The vulns-by-layer design duplicates `rootFs`
// and `history` onto every container scan result so the backend can
// perform the diffID -> instruction join per-monitor without a cross-scan-result
// lookup. The first milestone only attributes OS packages, so app scan
// results have no `dockerLayerDiffId`-labelled nodes to join against —
// attaching the facts now would be dead weight in `container-monitor-data`
// until the app-side label emission ships. Restore the block below once
// app-package attribution is in place.
//
// if (depsAnalysis.introducingLayerByPackage) {
// if (rootFsFact) {
// appDepsScanResult.facts.push(rootFsFact);
// }
// if (historyFact) {
// appDepsScanResult.facts.push(historyFact);
// }
// }
if (ownershipEnabled && apkPathIndex) {
// One ownership fact per app-dep result; candidates fail closed independently.
const ownership = (0, apk_ownership_1.resolveApkOwnership)((0, evidence_paths_1.buildOwnershipCandidates)(appDepsScanResult), apkPathIndex, apkSymlinkGraph, osRelease);
if (ownership) {
const ownershipFact = {
type: "apkPackageOwnership",
data: ownership,
};
appDepsScanResult.facts.push(ownershipFact);
}
}
// Drop the internal-only install-dir data so it never reaches the wire.
const { nodeModulesPackagePaths: _internal, ...publicResult } = appDepsScanResult;
return {
...publicResult,
target: {
image: depGraph.rootPkg.name,
},
...(options &&
options["target-reference"] && {
targetReference: options["target-reference"],
}),
};
});
const args = depsAnalysis.platform !== undefined
? { platform: depsAnalysis.platform }
: undefined;
const depGraphFact = {
type: "depGraph",
data: depGraph,
};
if (names) {
if (names.length > 0) {
const imageNameInfo = { names };
const imageNamesFact = {
type: "imageNames",
data: imageNameInfo,
};
additionalFacts.push(imageNamesFact);
}
}
if (ociDistributionMetadata) {
const metadataFact = {
type: "ociDistributionMetadata",
data: ociDistributionMetadata,
};
additionalFacts.push(metadataFact);
}
if (depsAnalysis.attestations && depsAnalysis.attestations.length > 0) {
const parsed = await (0, provenance_parser_1.parseProvenanceAttestations)(depsAnalysis.attestations);
if (parsed.length > 0) {
const provenanceMetadataFact = {
type: "provenanceMetadata",
data: parsed,
};
additionalFacts.push(provenanceMetadataFact);
}
}
if (depsAnalysis.platform) {
const platformFact = {
type: "platform",
data: depsAnalysis.platform,
};
additionalFacts.push(platformFact);
}
const pluginVersionFact = {
type: "pluginVersion",
data: version_1.PLUGIN_VERSION,
};
additionalFacts.push(pluginVersionFact);
const pluginWarningsData = {};
if ((options === null || options === void 0 ? void 0 : options.parameterWarnings) && options.parameterWarnings.length > 0) {
pluginWarningsData.parameterChecks = options.parameterWarnings;
}
if ((_g = depsAnalysis.layerAttributionWarnings) === null || _g === void 0 ? void 0 : _g.length) {
pluginWarningsData.layerAttribution = depsAnalysis.layerAttributionWarnings;
}
if (Object.keys(pluginWarningsData).length > 0) {
const pluginWarningsFact = {
type: "pluginWarnings",
data: pluginWarningsData,
};
additionalFacts.push(pluginWarningsFact);
}
const scanResults = [
{
facts: [depGraphFact, ...additionalFacts],
target: {
image: depGraph.rootPkg.name,
},
identity: {
type: depGraph.pkgManager.name,
args,
},
...(options &&
options["target-reference"] && {
targetReference: (_h = options["target-reference"]) !== null && _h !== void 0 ? _h : depGraph.rootPkg.name,
}),
},
...applicationDependenciesScanResults,
];
const truncatedScanResults = scanResults.map((result) => ({
...result,
facts: (0, utils_1.truncateAdditionalFacts)(result.facts || []),
}));
const scanPayloadMetrics = (0, scan_payload_metrics_1.computeScanPayloadMetrics)(truncatedScanResults);
return {
scanResults: truncatedScanResults,
analytics: [
{
name: "containerScanPayloadMetrics",
data: scanPayloadMetrics,
},
],
};
}
exports.buildResponse = buildResponse;
/**
* Expands the provided dockerfile packages to include transitive dependencies.
* Transitive dependencies are keyed by their source segments.
*
* @important
* mutates the provided `dockerfilePackages` object.
*
* @warning
* **Known Issue:** In some scenarios, this function can cause over-attribution of
* dependencies to the dockerfile because the `dockerfilePackages` object is mutated
* while iterating. This behavior is retained for downstream compatibility.
*
* @param dockerfilePackages - The dockerfile packages to expand.
* @param dependencies - The dependencies of the image.
* @returns The expanded dockerfile packages.
*/
function getUserInstructionDeps(dockerfilePackages, dependencies) {
for (const dependencyName in dependencies) {
if (dependencies.hasOwnProperty(dependencyName)) {
const sourceOrName = dependencyName.split("/")[0];
const dockerfilePackage = dockerfilePackages[sourceOrName];
if (dockerfilePackage) {
for (const dep of collectDeps(dependencies[dependencyName])) {
dockerfilePackages[dep.split("/")[0]] = { ...dockerfilePackage };
}
}
}
}
return dockerfilePackages;
}
function collectDeps(pkg) {
// ES5 doesn't have Object.values, so replace with Object.keys() and map()
return pkg.dependencies
? Object.keys(pkg.dependencies)
.map((name) => pkg.dependencies[name])
.reduce((allDeps, pkg) => {
return [...allDeps, ...collectDeps(pkg)];
}, Object.keys(pkg.dependencies))
: [];
}
/**
* Excludes base image dependencies from the dependency tree if excludeBaseImageVulns is true.
*
* @param deps - The dependencies of the image.
* @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
* @param excludeBaseImageVulns - Whether to exclude base image dependencies.
* @returns The dependencies of the image.
*/
function excludeBaseImageDeps(deps, dockerfilePkgs, excludeBaseImageVulns) {
if (!excludeBaseImageVulns || !dockerfilePkgs) {
return deps;
}
return Object.keys(deps)
.filter((depName) => {
if (dockerfilePkgs[depName] !== undefined) {
return true;
}
const source = depName.split("/")[0];
return dockerfilePkgs[source] !== undefined;
})
.reduce((extractedDeps, depName) => {
extractedDeps[depName] = deps[depName];
return extractedDeps;
}, {});
}
/**
* Annotates the dependencies with the layer ID of the Dockerfile
* instruction that installed them.
*
* @important
* mutates the provided `deps` object.
*
* @param deps - The dependencies of the image.
* @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
*/
function annotateLayerIds(deps, dockerfilePkgs) {
if (!dockerfilePkgs) {
return;
}
for (const dep of Object.keys(deps)) {
const pkg = deps[dep];
const pkgSource = dep.split("/")[0];
const dockerfilePkg = dockerfilePkgs[dep] || dockerfilePkgs[pkgSource];
if (dockerfilePkg) {
pkg.labels = {
...(pkg.labels || {}),
dockerLayerId: (0, dockerfile_1.instructionDigest)(dockerfilePkg.instruction),
};
}
if (pkg.dependencies) {
annotateLayerIds(pkg.dependencies, dockerfilePkgs);
}
}
}
/**
* Walks the dep tree and stamps `dockerLayerDiffId` on every node that has
* an entry in the package -> diffID map produced by
* `computeOsLayerAttribution`. The label survives `legacy.depTreeToGraph`
* conversion and surfaces as `node.info.labels.dockerLayerDiffId` on the
* resulting dep-graph node — the contract the backend's read-path
* join depends on.
*
* Lookup key shape (`${name}@${version}`) matches what the attribution
* producer mints via `depFullName(pkg)@${version}`; the dep-tree builder
* uses the same `depFullName` for its node names, so the join is direct.
*
* @important mutates the provided `deps` object.
*/
function annotateDockerLayerDiffIds(deps, introducingLayerByPackage) {
for (const depName of Object.keys(deps)) {
const pkg = deps[depName];
const diffID = introducingLayerByPackage.get(`${pkg.name}@${pkg.version}`);
if (diffID) {
pkg.labels = {
...(pkg.labels || {}),
dockerLayerDiffId: diffID,
};
}
if (pkg.dependencies) {
annotateDockerLayerDiffIds(pkg.dependencies, introducingLayerByPackage);
}
}
}
//# sourceMappingURL=response-builder.js.map