UNPKG

snyk-docker-plugin

Version:
438 lines (391 loc) 13.1 kB
import { legacy } from "@snyk/dep-graph"; import { StaticAnalysis } from "./analyzer/types"; import * as facts from "./facts"; // Module that provides functions to collect and build response after all // analyses' are done. import { instructionDigest } from "./dockerfile"; import { DockerFileAnalysis, DockerFilePackages } from "./dockerfile/types"; import { OCIDistributionMetadata } from "./extractor/oci-distribution-metadata"; import * as types from "./types"; import { truncateAdditionalFacts } from "./utils"; import { PLUGIN_VERSION } from "./version"; export { buildResponse }; async function buildResponse( depsAnalysis: StaticAnalysis & { depTree: types.DepTree; packageFormat: string; }, dockerfileAnalysis: DockerFileAnalysis | undefined, excludeBaseImageVulns: boolean, names?: string[], ociDistributionMetadata?: OCIDistributionMetadata, options?: Partial<types.PluginOptions>, ): Promise<types.PluginResponse> { const deps = depsAnalysis.depTree.dependencies; const dockerfilePkgs = collectDockerfilePkgs(dockerfileAnalysis, deps); const finalDeps = excludeBaseImageDeps( deps, dockerfilePkgs, excludeBaseImageVulns, ); /** WARNING! Mutates the depTree.dependencies! */ annotateLayerIds(finalDeps, dockerfilePkgs); // Apply the filtered dependencies back to the depTree depsAnalysis.depTree.dependencies = finalDeps; /** This must be called after all final changes to the DependencyTree. */ const depGraph = await legacy.depTreeToGraph( depsAnalysis.depTree, depsAnalysis.packageFormat, ); const additionalFacts: types.Fact[] = []; const hashes = depsAnalysis.binaries; if (hashes && hashes.length > 0) { const keyBinariesHashesFact: facts.KeyBinariesHashesFact = { type: "keyBinariesHashes", data: hashes, }; additionalFacts.push(keyBinariesHashesFact); } if (dockerfileAnalysis !== undefined) { const dockerfileAnalysisFact: facts.DockerfileAnalysisFact = { type: "dockerfileAnalysis", data: dockerfileAnalysis, }; additionalFacts.push(dockerfileAnalysisFact); } if (depsAnalysis.imageId) { const imageIdFact: facts.ImageIdFact = { type: "imageId", data: depsAnalysis.imageId, }; additionalFacts.push(imageIdFact); } if (depsAnalysis.imageLayers && depsAnalysis.imageLayers.length > 0) { const imageLayersFact: facts.ImageLayersFact = { type: "imageLayers", data: depsAnalysis.imageLayers, }; additionalFacts.push(imageLayersFact); } if (depsAnalysis.imageLabels) { const imageLabels: facts.ImageLabels = { type: "imageLabels", data: depsAnalysis.imageLabels, }; additionalFacts.push(imageLabels); } if (depsAnalysis.containerConfig) { const containerConfigFact: facts.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); } if (depsAnalysis.history && depsAnalysis.history.length > 0) { const historyFact: facts.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: facts.ImageCreationTimeFact = { type: "imageCreationTime", data: depsAnalysis.imageCreationTime, }; additionalFacts.push(imageCreationTimeFact); } if ( depsAnalysis.rootFsLayers && Array.isArray(depsAnalysis.rootFsLayers) && depsAnalysis.rootFsLayers.length > 0 ) { const rootFsFact: facts.RootFsFact = { type: "rootFs", data: depsAnalysis.rootFsLayers, }; additionalFacts.push(rootFsFact); } if (depsAnalysis.depTree.targetOS.prettyName) { const imageOsReleasePrettyNameFact: facts.ImageOsReleasePrettyNameFact = { type: "imageOsReleasePrettyName", data: depsAnalysis.depTree.targetOS.prettyName, }; additionalFacts.push(imageOsReleasePrettyNameFact); } const manifestFiles = depsAnalysis.manifestFiles.length > 0 ? depsAnalysis.manifestFiles : undefined; if (manifestFiles) { const imageManifestFilesFact: facts.ImageManifestFilesFact = { type: "imageManifestFiles", data: manifestFiles, }; additionalFacts.push(imageManifestFilesFact); } const autoDetectedPackages = depsAnalysis.autoDetectedUserInstructions?.dockerfilePackages; const autoDetectedLayers = depsAnalysis.autoDetectedUserInstructions?.dockerfileLayers; if ( autoDetectedPackages && Object.keys(autoDetectedPackages).length > 0 && autoDetectedLayers && Object.keys(autoDetectedLayers).length > 0 ) { const autoDetectedPackagesWithChildren = getUserInstructionDeps( autoDetectedPackages, deps, ); const autoDetectedUserInstructionsFact: facts.AutoDetectedUserInstructionsFact = { type: "autoDetectedUserInstructions", data: { dockerfileLayers: autoDetectedLayers, dockerfilePackages: autoDetectedPackagesWithChildren!, }, }; additionalFacts.push(autoDetectedUserInstructionsFact); } const applicationDependenciesScanResults: types.ScanResult[] = ( depsAnalysis.applicationDependenciesScanResults || [] ).map((appDepsScanResult) => { if (depsAnalysis.imageId) { const imageIdFact: facts.ImageIdFact = { type: "imageId", data: depsAnalysis.imageId, }; appDepsScanResult.facts.push(imageIdFact); } if (names && names.length > 0) { const imageNamesFact: facts.ImageNamesFact = { type: "imageNames", data: { names }, }; appDepsScanResult.facts.push(imageNamesFact); } if (ociDistributionMetadata) { const metadataFact: facts.OCIDistributionMetadataFact = { type: "ociDistributionMetadata", data: ociDistributionMetadata, }; appDepsScanResult.facts.push(metadataFact); } const appPluginVersionFact: facts.PluginVersionFact = { type: "pluginVersion", data: PLUGIN_VERSION, }; appDepsScanResult.facts.push(appPluginVersionFact); return { ...appDepsScanResult, target: { image: depGraph.rootPkg.name, }, ...(options && options["target-reference"] && { targetReference: options["target-reference"], }), }; }); const args = depsAnalysis.platform !== undefined ? { platform: depsAnalysis.platform } : undefined; const depGraphFact: facts.DepGraphFact = { type: "depGraph", data: depGraph, }; if (names) { if (names.length > 0) { const imageNameInfo = { names }; const imageNamesFact: facts.ImageNamesFact = { type: "imageNames", data: imageNameInfo, }; additionalFacts.push(imageNamesFact); } } if (ociDistributionMetadata) { const metadataFact: facts.OCIDistributionMetadataFact = { type: "ociDistributionMetadata", data: ociDistributionMetadata, }; additionalFacts.push(metadataFact); } if (depsAnalysis.platform) { const platformFact: facts.PlatformFact = { type: "platform", data: depsAnalysis.platform, }; additionalFacts.push(platformFact); } const pluginVersionFact: facts.PluginVersionFact = { type: "pluginVersion", data: PLUGIN_VERSION, }; additionalFacts.push(pluginVersionFact); if (options?.parameterWarnings && options.parameterWarnings.length > 0) { const pluginWarningsFact: facts.PluginWarningsFact = { type: "pluginWarnings", data: { parameterChecks: options.parameterWarnings, }, }; additionalFacts.push(pluginWarningsFact); } const scanResults: types.ScanResult[] = [ { facts: [depGraphFact, ...additionalFacts], target: { image: depGraph.rootPkg.name, }, identity: { type: depGraph.pkgManager.name, args, }, ...(options && options["target-reference"] && { targetReference: options["target-reference"] ?? depGraph.rootPkg.name, }), }, ...applicationDependenciesScanResults, ]; const truncatedScanResults = scanResults.map((result) => ({ ...result, facts: truncateAdditionalFacts(result.facts || []), })); return { scanResults: truncatedScanResults, }; } function collectDockerfilePkgs( dockerAnalysis: DockerFileAnalysis | undefined, deps: { [depName: string]: types.DepTreeDep; }, ) { if (!dockerAnalysis) { return; } return getUserInstructionDeps(dockerAnalysis.dockerfilePackages, deps); } // Iterate over the dependencies list; if one is introduced by the dockerfile, // flatten its dependencies and append them to the list of dockerfile // packages. This gives us a reference of all transitive deps installed via // the dockerfile, and the instruction that installed it. function getUserInstructionDeps( dockerfilePackages: DockerFilePackages, dependencies: { [depName: string]: types.DepTreeDep; }, ): DockerFilePackages { 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)) : []; } // Skip processing if option disabled or dockerfilePkgs is undefined. We // can't exclude anything in that case, because we can't tell which deps are // from dockerfile and which from base image. function excludeBaseImageDeps( deps: { [depName: string]: types.DepTreeDep; }, dockerfilePkgs: DockerFilePackages | undefined, excludeBaseImageVulns: boolean, ) { if (!excludeBaseImageVulns || !dockerfilePkgs) { return deps; } return extractDockerfileDeps(deps, dockerfilePkgs); } function extractDockerfileDeps( allDeps: { [depName: string]: types.DepTreeDep; }, dockerfilePkgs: DockerFilePackages, ) { return Object.keys(allDeps) .filter((depName) => dockerfilePkgs[depName]) .reduce((extractedDeps, depName) => { extractedDeps[depName] = allDeps[depName]; return extractedDeps; }, {}); } function annotateLayerIds(deps, dockerfilePkgs) { if (!dockerfilePkgs) { return; } for (const dep of Object.keys(deps)) { const pkg = deps[dep]; const dockerfilePkg = dockerfilePkgs[dep]; if (dockerfilePkg) { pkg.labels = { ...(pkg.labels || {}), dockerLayerId: instructionDigest(dockerfilePkg.instruction), }; } if (pkg.dependencies) { annotateLayerIds(pkg.dependencies, dockerfilePkgs); } } }