Skip to content

Commit cdfde65

Browse files
committed
fix: update dockerfile attribution logic
1 parent 1175cc6 commit cdfde65

2 files changed

Lines changed: 344 additions & 71 deletions

File tree

lib/response-builder.ts

Lines changed: 99 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,19 @@ async function buildResponse(
2626
options?: Partial<types.PluginOptions>,
2727
): Promise<types.PluginResponse> {
2828
const deps = depsAnalysis.depTree.dependencies;
29-
const dockerfilePkgs = collectDockerfilePkgs(dockerfileAnalysis, deps);
29+
const dockerfilePkgs = dockerfileAnalysis?.dockerfilePackages ?? {};
30+
31+
/** WARNING! Mutates the depTree.dependencies! */
32+
annotateLayerIds(deps, dockerfilePkgs);
33+
3034
const finalDeps = excludeBaseImageDeps(
3135
deps,
3236
dockerfilePkgs,
3337
excludeBaseImageVulns,
3438
);
35-
/** WARNING! Mutates the depTree.dependencies! */
36-
annotateLayerIds(finalDeps, dockerfilePkgs);
39+
40+
// Apply the filtered dependencies back to the depTree
41+
depsAnalysis.depTree.dependencies = finalDeps;
3742

3843
/** This must be called after all final changes to the DependencyTree. */
3944
const depGraph = await legacy.depTreeToGraph(
@@ -190,7 +195,7 @@ async function buildResponse(
190195
autoDetectedLayers &&
191196
Object.keys(autoDetectedLayers).length > 0
192197
) {
193-
const autoDetectedPackagesWithChildren = getUserInstructionDeps(
198+
const autoDetectedPackagesWithChildren = mapDepTreeToDockerfilePackages(
194199
autoDetectedPackages,
195200
deps,
196201
);
@@ -332,59 +337,59 @@ async function buildResponse(
332337
};
333338
}
334339

335-
function collectDockerfilePkgs(
336-
dockerAnalysis: DockerFileAnalysis | undefined,
337-
deps: {
338-
[depName: string]: types.DepTreeDep;
339-
},
340-
) {
341-
if (!dockerAnalysis) {
342-
return;
343-
}
340+
// Returns the package source name from a dependency key. A package source refers
341+
// to the top-level Linux package name, such as "bzip2" in "bzip2/libbz2-dev".
342+
function packageSource(depKey: string): string {
343+
return depKey.split("/")[0];
344+
}
344345

345-
return getUserInstructionDeps(dockerAnalysis.dockerfilePackages, deps);
346+
function collectTransitiveDepKeys(pkg: types.DepTreeDep): string[] {
347+
if (!pkg.dependencies || Object.keys(pkg.dependencies).length === 0) {
348+
return [];
349+
}
350+
const keys = Object.keys(pkg.dependencies);
351+
const nested: string[] = [];
352+
for (const key of keys) {
353+
const childKeys = collectTransitiveDepKeys(pkg.dependencies![key]);
354+
for (const childKey of childKeys) {
355+
nested.push(childKey);
356+
}
357+
}
358+
return keys.concat(nested);
346359
}
347360

348-
// Iterate over the dependencies list; if one is introduced by the dockerfile,
349-
// flatten its dependencies and append them to the list of dockerfile
350-
// packages. This gives us a reference of all transitive deps installed via
351-
// the dockerfile, and the instruction that installed it.
352-
function getUserInstructionDeps(
353-
dockerfilePackages: DockerFilePackages,
354-
dependencies: {
355-
[depName: string]: types.DepTreeDep;
356-
},
361+
// Maps each dependency key (and its transitives) that matches a dockerfile-
362+
// installed package to that package's instruction.
363+
export function mapDepTreeToDockerfilePackages(
364+
dockerfilePkgs: DockerFilePackages,
365+
deps: { [depName: string]: types.DepTreeDep },
357366
): DockerFilePackages {
358-
for (const dependencyName in dependencies) {
359-
if (dependencies.hasOwnProperty(dependencyName)) {
360-
const sourceOrName = dependencyName.split("/")[0];
361-
const dockerfilePackage = dockerfilePackages[sourceOrName];
362-
363-
if (dockerfilePackage) {
364-
for (const dep of collectDeps(dependencies[dependencyName])) {
365-
dockerfilePackages[dep.split("/")[0]] = { ...dockerfilePackage };
366-
}
367-
}
368-
}
367+
if (!dockerfilePkgs) {
368+
return {};
369369
}
370370

371-
return dockerfilePackages;
372-
}
371+
for (const rootKey of Object.keys(deps)) {
372+
const source = packageSource(rootKey);
373+
const instruction = dockerfilePkgs[rootKey] || dockerfilePkgs[source];
374+
if (!instruction) {
375+
continue;
376+
}
377+
378+
// Ensure the instruction data is stored under the key that matches the
379+
// dependency tree.
380+
dockerfilePkgs[rootKey] = instruction;
381+
382+
const transitiveKeys = collectTransitiveDepKeys(deps[rootKey]);
383+
for (const key of transitiveKeys) {
384+
dockerfilePkgs[key] = instruction;
385+
}
386+
}
373387

374-
function collectDeps(pkg) {
375-
// ES5 doesn't have Object.values, so replace with Object.keys() and map()
376-
return pkg.dependencies
377-
? Object.keys(pkg.dependencies)
378-
.map((name) => pkg.dependencies[name])
379-
.reduce((allDeps, pkg) => {
380-
return [...allDeps, ...collectDeps(pkg)];
381-
}, Object.keys(pkg.dependencies))
382-
: [];
388+
return dockerfilePkgs;
383389
}
384390

385-
// Skip processing if option disabled or dockerfilePkgs is undefined. We
386-
// can't exclude anything in that case, because we can't tell which deps are
387-
// from dockerfile and which from base image.
391+
// If excludeBaseImageVulns is true, only retain dependencies that are
392+
// dockerfile-introduced, as defined by dockerfilePkgs.
388393
function excludeBaseImageDeps(
389394
deps: {
390395
[depName: string]: types.DepTreeDep;
@@ -396,39 +401,62 @@ function excludeBaseImageDeps(
396401
return deps;
397402
}
398403

399-
return extractDockerfileDeps(deps, dockerfilePkgs);
400-
}
401-
402-
function extractDockerfileDeps(
403-
allDeps: {
404-
[depName: string]: types.DepTreeDep;
405-
},
406-
dockerfilePkgs: DockerFilePackages,
407-
) {
408-
return Object.keys(allDeps)
409-
.filter((depName) => dockerfilePkgs[depName])
404+
return Object.keys(deps)
405+
.filter(
406+
(depName) =>
407+
dockerfilePkgs[depName] || dockerfilePkgs[packageSource(depName)],
408+
)
410409
.reduce((extractedDeps, depName) => {
411-
extractedDeps[depName] = allDeps[depName];
410+
extractedDeps[depName] = deps[depName];
412411
return extractedDeps;
413412
}, {});
414413
}
415414

416-
function annotateLayerIds(deps, dockerfilePkgs) {
415+
// Annotates dockerfile-introduced dependencies and sub-dependencies with the
416+
// instruction ID. A dependency is identified as dockerfile-introduced if the
417+
// dependency key or source was found in a dockerfile installation instruction.
418+
function annotateLayerIds(
419+
deps: { [depName: string]: types.DepTreeDep },
420+
dockerfilePkgs: DockerFilePackages | undefined,
421+
): void {
417422
if (!dockerfilePkgs) {
418423
return;
419424
}
420425

421-
for (const dep of Object.keys(deps)) {
422-
const pkg = deps[dep];
423-
const dockerfilePkg = dockerfilePkgs[dep];
424-
if (dockerfilePkg) {
425-
pkg.labels = {
426-
...(pkg.labels || {}),
427-
dockerLayerId: instructionDigest(dockerfilePkg.instruction),
428-
};
426+
for (const rootKey of Object.keys(deps)) {
427+
const source = packageSource(rootKey);
428+
const dockerfileEntry = dockerfilePkgs[rootKey] || dockerfilePkgs[source];
429+
if (!dockerfileEntry) {
430+
continue;
431+
}
432+
433+
const rootNode = deps[rootKey];
434+
const layerId = instructionDigest(dockerfileEntry.instruction);
435+
rootNode.labels = {
436+
...(rootNode.labels || {}),
437+
dockerLayerId: layerId,
438+
};
439+
if (
440+
rootNode.dependencies &&
441+
Object.keys(rootNode.dependencies).length > 0
442+
) {
443+
annotateSubtreeWithLayerId(rootNode.dependencies, layerId);
429444
}
430-
if (pkg.dependencies) {
431-
annotateLayerIds(pkg.dependencies, dockerfilePkgs);
445+
}
446+
}
447+
448+
function annotateSubtreeWithLayerId(
449+
deps: { [depName: string]: types.DepTreeDep },
450+
dockerLayerId: string,
451+
): void {
452+
for (const depKey of Object.keys(deps)) {
453+
const node = deps[depKey];
454+
node.labels = {
455+
...(node.labels || {}),
456+
dockerLayerId,
457+
};
458+
if (node.dependencies && Object.keys(node.dependencies).length > 0) {
459+
annotateSubtreeWithLayerId(node.dependencies, dockerLayerId);
432460
}
433461
}
434462
}

0 commit comments

Comments
 (0)