Skip to content

Commit 1332f64

Browse files
committed
fix: update dockerfile attribution logic
1 parent c9cbe4e commit 1332f64

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(
@@ -197,7 +202,7 @@ async function buildResponse(
197202
autoDetectedLayers &&
198203
Object.keys(autoDetectedLayers).length > 0
199204
) {
200-
const autoDetectedPackagesWithChildren = getUserInstructionDeps(
205+
const autoDetectedPackagesWithChildren = mapDepTreeToDockerfilePackages(
201206
autoDetectedPackages,
202207
deps,
203208
);
@@ -339,59 +344,59 @@ async function buildResponse(
339344
};
340345
}
341346

342-
function collectDockerfilePkgs(
343-
dockerAnalysis: DockerFileAnalysis | undefined,
344-
deps: {
345-
[depName: string]: types.DepTreeDep;
346-
},
347-
) {
348-
if (!dockerAnalysis) {
349-
return;
350-
}
347+
// Returns the package source name from a dependency key. A package source refers
348+
// to the top-level Linux package name, such as "bzip2" in "bzip2/libbz2-dev".
349+
function packageSource(depKey: string): string {
350+
return depKey.split("/")[0];
351+
}
351352

352-
return getUserInstructionDeps(dockerAnalysis.dockerfilePackages, deps);
353+
function collectTransitiveDepKeys(pkg: types.DepTreeDep): string[] {
354+
if (!pkg.dependencies || Object.keys(pkg.dependencies).length === 0) {
355+
return [];
356+
}
357+
const keys = Object.keys(pkg.dependencies);
358+
const nested: string[] = [];
359+
for (const key of keys) {
360+
const childKeys = collectTransitiveDepKeys(pkg.dependencies![key]);
361+
for (const childKey of childKeys) {
362+
nested.push(childKey);
363+
}
364+
}
365+
return keys.concat(nested);
353366
}
354367

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

378-
return dockerfilePackages;
379-
}
378+
for (const rootKey of Object.keys(deps)) {
379+
const source = packageSource(rootKey);
380+
const instruction = dockerfilePkgs[rootKey] || dockerfilePkgs[source];
381+
if (!instruction) {
382+
continue;
383+
}
384+
385+
// Ensure the instruction data is stored under the key that matches the
386+
// dependency tree.
387+
dockerfilePkgs[rootKey] = instruction;
388+
389+
const transitiveKeys = collectTransitiveDepKeys(deps[rootKey]);
390+
for (const key of transitiveKeys) {
391+
dockerfilePkgs[key] = instruction;
392+
}
393+
}
380394

381-
function collectDeps(pkg) {
382-
// ES5 doesn't have Object.values, so replace with Object.keys() and map()
383-
return pkg.dependencies
384-
? Object.keys(pkg.dependencies)
385-
.map((name) => pkg.dependencies[name])
386-
.reduce((allDeps, pkg) => {
387-
return [...allDeps, ...collectDeps(pkg)];
388-
}, Object.keys(pkg.dependencies))
389-
: [];
395+
return dockerfilePkgs;
390396
}
391397

392-
// Skip processing if option disabled or dockerfilePkgs is undefined. We
393-
// can't exclude anything in that case, because we can't tell which deps are
394-
// from dockerfile and which from base image.
398+
// If excludeBaseImageVulns is true, only retain dependencies that are
399+
// dockerfile-introduced, as defined by dockerfilePkgs.
395400
function excludeBaseImageDeps(
396401
deps: {
397402
[depName: string]: types.DepTreeDep;
@@ -403,39 +408,62 @@ function excludeBaseImageDeps(
403408
return deps;
404409
}
405410

406-
return extractDockerfileDeps(deps, dockerfilePkgs);
407-
}
408-
409-
function extractDockerfileDeps(
410-
allDeps: {
411-
[depName: string]: types.DepTreeDep;
412-
},
413-
dockerfilePkgs: DockerFilePackages,
414-
) {
415-
return Object.keys(allDeps)
416-
.filter((depName) => dockerfilePkgs[depName])
411+
return Object.keys(deps)
412+
.filter(
413+
(depName) =>
414+
dockerfilePkgs[depName] || dockerfilePkgs[packageSource(depName)],
415+
)
417416
.reduce((extractedDeps, depName) => {
418-
extractedDeps[depName] = allDeps[depName];
417+
extractedDeps[depName] = deps[depName];
419418
return extractedDeps;
420419
}, {});
421420
}
422421

423-
function annotateLayerIds(deps, dockerfilePkgs) {
422+
// Annotates dockerfile-introduced dependencies and sub-dependencies with the
423+
// instruction ID. A dependency is identified as dockerfile-introduced if the
424+
// dependency key or source was found in a dockerfile installation instruction.
425+
function annotateLayerIds(
426+
deps: { [depName: string]: types.DepTreeDep },
427+
dockerfilePkgs: DockerFilePackages | undefined,
428+
): void {
424429
if (!dockerfilePkgs) {
425430
return;
426431
}
427432

428-
for (const dep of Object.keys(deps)) {
429-
const pkg = deps[dep];
430-
const dockerfilePkg = dockerfilePkgs[dep];
431-
if (dockerfilePkg) {
432-
pkg.labels = {
433-
...(pkg.labels || {}),
434-
dockerLayerId: instructionDigest(dockerfilePkg.instruction),
435-
};
433+
for (const rootKey of Object.keys(deps)) {
434+
const source = packageSource(rootKey);
435+
const dockerfileEntry = dockerfilePkgs[rootKey] || dockerfilePkgs[source];
436+
if (!dockerfileEntry) {
437+
continue;
438+
}
439+
440+
const rootNode = deps[rootKey];
441+
const layerId = instructionDigest(dockerfileEntry.instruction);
442+
rootNode.labels = {
443+
...(rootNode.labels || {}),
444+
dockerLayerId: layerId,
445+
};
446+
if (
447+
rootNode.dependencies &&
448+
Object.keys(rootNode.dependencies).length > 0
449+
) {
450+
annotateSubtreeWithLayerId(rootNode.dependencies, layerId);
436451
}
437-
if (pkg.dependencies) {
438-
annotateLayerIds(pkg.dependencies, dockerfilePkgs);
452+
}
453+
}
454+
455+
function annotateSubtreeWithLayerId(
456+
deps: { [depName: string]: types.DepTreeDep },
457+
dockerLayerId: string,
458+
): void {
459+
for (const depKey of Object.keys(deps)) {
460+
const node = deps[depKey];
461+
node.labels = {
462+
...(node.labels || {}),
463+
dockerLayerId,
464+
};
465+
if (node.dependencies && Object.keys(node.dependencies).length > 0) {
466+
annotateSubtreeWithLayerId(node.dependencies, dockerLayerId);
439467
}
440468
}
441469
}

0 commit comments

Comments
 (0)