Skip to content

Commit 7349c56

Browse files
committed
fix: revert dockerfile packages facts to original state
1 parent 5dd84e9 commit 7349c56

File tree

2 files changed

+773
-53
lines changed

2 files changed

+773
-53
lines changed

lib/response-builder.ts

Lines changed: 144 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import * as types from "./types";
1212
import { truncateAdditionalFacts } from "./utils";
1313
import { PLUGIN_VERSION } from "./version";
1414

15-
export { buildResponse };
15+
export {
16+
buildResponse,
17+
annotateWithLayerIds,
18+
expandDockerfilePackages,
19+
excludeBaseImageDeps,
20+
};
1621

1722
async function buildResponse(
1823
depsAnalysis: StaticAnalysis & {
@@ -26,14 +31,27 @@ async function buildResponse(
2631
options?: Partial<types.PluginOptions>,
2732
): Promise<types.PluginResponse> {
2833
const deps = depsAnalysis.depTree.dependencies;
29-
const dockerfilePkgs = collectDockerfilePkgs(dockerfileAnalysis, deps);
30-
const finalDeps = excludeBaseImageDeps(
31-
deps,
32-
dockerfilePkgs,
33-
excludeBaseImageVulns,
34-
);
35-
/** WARNING! Mutates the depTree.dependencies! */
36-
annotateLayerIds(finalDeps, dockerfilePkgs);
34+
35+
const inputDockerfilePkgs =
36+
dockerfileAnalysis?.dockerfilePackages ??
37+
depsAnalysis.autoDetectedUserInstructions?.dockerfilePackages;
38+
39+
if (inputDockerfilePkgs) {
40+
const expandedDockerfilePkgs = expandDockerfilePackages(
41+
inputDockerfilePkgs,
42+
deps,
43+
);
44+
45+
const finalDeps = excludeBaseImageDeps(
46+
deps,
47+
expandedDockerfilePkgs,
48+
excludeBaseImageVulns,
49+
);
50+
annotateWithLayerIds(finalDeps, expandedDockerfilePkgs);
51+
52+
// Assign the final dependencies to the dependency tree.
53+
depsAnalysis.depTree.dependencies = finalDeps;
54+
}
3755

3856
/** This must be called after all final changes to the DependencyTree. */
3957
const depGraph = await legacy.depTreeToGraph(
@@ -53,6 +71,9 @@ async function buildResponse(
5371
}
5472

5573
if (dockerfileAnalysis !== undefined) {
74+
// Use legacy expansion function to maintain downstream compatibility.
75+
getUserInstructionDeps(dockerfileAnalysis.dockerfilePackages, deps);
76+
5677
const dockerfileAnalysisFact: facts.DockerfileAnalysisFact = {
5778
type: "dockerfileAnalysis",
5879
data: dockerfileAnalysis,
@@ -332,23 +353,23 @@ async function buildResponse(
332353
};
333354
}
334355

335-
function collectDockerfilePkgs(
336-
dockerAnalysis: DockerFileAnalysis | undefined,
337-
deps: {
338-
[depName: string]: types.DepTreeDep;
339-
},
340-
) {
341-
if (!dockerAnalysis) {
342-
return;
343-
}
344-
345-
return getUserInstructionDeps(dockerAnalysis.dockerfilePackages, deps);
346-
}
347-
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.
356+
/**
357+
* @deprecated This function is deprecated and will be removed in a future version.
358+
* Expands the provided dockerfile packages to include transitive dependencies.
359+
* Transitive dependencies are keyed by their source segments.
360+
*
361+
* @important
362+
* mutates the provided `dockerfilePackages` object.
363+
*
364+
* @warning
365+
* **Known Issue:** In some scenarios, this function can cause over-attribution of
366+
* dependencies to the dockerfile because the `dockerfilePackages` object is mutated
367+
* while iterating. This behavior is retained for downstream compatibility.
368+
*
369+
* @param dockerfilePackages - The dockerfile packages to expand.
370+
* @param dependencies - The dependencies of the image.
371+
* @returns The expanded dockerfile packages.
372+
*/
352373
function getUserInstructionDeps(
353374
dockerfilePackages: DockerFilePackages,
354375
dependencies: {
@@ -362,6 +383,7 @@ function getUserInstructionDeps(
362383

363384
if (dockerfilePackage) {
364385
for (const dep of collectDeps(dependencies[dependencyName])) {
386+
// Transitive dependencies are keyed by their source segments.
365387
dockerfilePackages[dep.split("/")[0]] = { ...dockerfilePackage };
366388
}
367389
}
@@ -382,9 +404,67 @@ function collectDeps(pkg) {
382404
: [];
383405
}
384406

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.
407+
/**
408+
* Returns the package source name from a full dependency name.
409+
*
410+
* A package source refers to the top-level package name, such as "foo" in "foo/foo-dev".
411+
*
412+
* @param depName - The full dependency name.
413+
* @returns The package source name.
414+
*/
415+
function packageSource(depName: string): string {
416+
return depName.split("/")[0];
417+
}
418+
419+
/**
420+
* Expands the list of packages explicitly requested in the Dockerfile to include all transitive dependencies.
421+
* The returned package map is keyed by the full dependency names.
422+
*
423+
* @param dockerfilePackages - The packages explicitly requested in a Dockerfile.
424+
* @param deps - The dependencies of the image.
425+
* @returns A map of packages attributed to the Dockerfile.
426+
*/
427+
function expandDockerfilePackages(
428+
dockerfilePackages: DockerFilePackages,
429+
deps: { [depName: string]: types.DepTreeDep },
430+
): DockerFilePackages {
431+
const expandedPkgs = {};
432+
433+
function collectChildPackages(node: types.DepTreeDep, parentEntry: any) {
434+
if (!node.dependencies) {
435+
return;
436+
}
437+
for (const childKey of Object.keys(node.dependencies)) {
438+
if (!expandedPkgs[childKey]) {
439+
expandedPkgs[childKey] = parentEntry;
440+
collectChildPackages(node.dependencies[childKey], parentEntry);
441+
}
442+
}
443+
}
444+
445+
for (const rootKey of Object.keys(deps)) {
446+
const source = packageSource(rootKey);
447+
const dockerfileEntry =
448+
dockerfilePackages[rootKey] || dockerfilePackages[source];
449+
if (dockerfileEntry) {
450+
// All keys in the expanded packages are the full dependency names.
451+
expandedPkgs[rootKey] = dockerfileEntry;
452+
453+
collectChildPackages(deps[rootKey], dockerfileEntry);
454+
}
455+
}
456+
457+
return expandedPkgs;
458+
}
459+
460+
/**
461+
* Excludes base image dependencies from the dependency tree if excludeBaseImageVulns is true.
462+
*
463+
* @param deps - The dependencies of the image.
464+
* @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
465+
* @param excludeBaseImageVulns - Whether to exclude base image dependencies.
466+
* @returns The dependencies of the image.
467+
*/
388468
function excludeBaseImageDeps(
389469
deps: {
390470
[depName: string]: types.DepTreeDep;
@@ -396,39 +476,51 @@ function excludeBaseImageDeps(
396476
return deps;
397477
}
398478

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)
479+
return Object.keys(deps)
409480
.filter((depName) => dockerfilePkgs[depName])
410481
.reduce((extractedDeps, depName) => {
411-
extractedDeps[depName] = allDeps[depName];
482+
extractedDeps[depName] = deps[depName];
412483
return extractedDeps;
413484
}, {});
414485
}
415486

416-
function annotateLayerIds(deps, dockerfilePkgs) {
487+
/**
488+
* Annotates the dependency tree with layer IDs.
489+
*
490+
* @important
491+
* mutates the provided `deps` object.
492+
*
493+
* @param deps - The dependencies of the image.
494+
* @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
495+
*/
496+
function annotateWithLayerIds(
497+
deps: { [depName: string]: types.DepTreeDep },
498+
dockerfilePkgs: DockerFilePackages | undefined,
499+
): void {
417500
if (!dockerfilePkgs) {
418501
return;
419502
}
420503

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-
};
429-
}
430-
if (pkg.dependencies) {
431-
annotateLayerIds(pkg.dependencies, dockerfilePkgs);
504+
function annotateRecursive(currentDeps: {
505+
[depName: string]: types.DepTreeDep;
506+
}) {
507+
for (const depKey of Object.keys(currentDeps)) {
508+
const node = currentDeps[depKey];
509+
const dockerfileEntry = dockerfilePkgs![depKey];
510+
511+
if (dockerfileEntry) {
512+
node.labels = {
513+
...(node.labels || {}),
514+
dockerLayerId: instructionDigest(dockerfileEntry.instruction),
515+
};
516+
517+
// Only progress down the dependency tree if the current node is a dockerfile package.
518+
if (node.dependencies) {
519+
annotateRecursive(node.dependencies);
520+
}
521+
}
432522
}
433523
}
524+
525+
annotateRecursive(deps);
434526
}

0 commit comments

Comments
 (0)