UNPKG

snyk-docker-plugin

Version:
471 lines 19.8 kB
"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 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.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