snyk-docker-plugin
Version:
Snyk CLI docker plugin
438 lines (391 loc) • 13.1 kB
text/typescript
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);
}
}
}