Skip to content

Commit 0969ac7

Browse files
committed
fix: revert dockerfile packages facts to original state
1 parent 7519d7e commit 0969ac7

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(
@@ -60,6 +78,9 @@ async function buildResponse(
6078
}
6179

6280
if (dockerfileAnalysis !== undefined) {
81+
// Use legacy expansion function to maintain downstream compatibility.
82+
getUserInstructionDeps(dockerfileAnalysis.dockerfilePackages, deps);
83+
6384
const dockerfileAnalysisFact: facts.DockerfileAnalysisFact = {
6485
type: "dockerfileAnalysis",
6586
data: dockerfileAnalysis,
@@ -339,23 +360,23 @@ async function buildResponse(
339360
};
340361
}
341362

342-
function collectDockerfilePkgs(
343-
dockerAnalysis: DockerFileAnalysis | undefined,
344-
deps: {
345-
[depName: string]: types.DepTreeDep;
346-
},
347-
) {
348-
if (!dockerAnalysis) {
349-
return;
350-
}
351-
352-
return getUserInstructionDeps(dockerAnalysis.dockerfilePackages, deps);
353-
}
354-
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.
363+
/**
364+
* @deprecated This function is deprecated and will be removed in a future version.
365+
* Expands the provided dockerfile packages to include transitive dependencies.
366+
* Transitive dependencies are keyed by their source segments.
367+
*
368+
* @important
369+
* mutates the provided `dockerfilePackages` object.
370+
*
371+
* @warning
372+
* **Known Issue:** In some scenarios, this function can cause over-attribution of
373+
* dependencies to the dockerfile because the `dockerfilePackages` object is mutated
374+
* while iterating. This behavior is retained for downstream compatibility.
375+
*
376+
* @param dockerfilePackages - The dockerfile packages to expand.
377+
* @param dependencies - The dependencies of the image.
378+
* @returns The expanded dockerfile packages.
379+
*/
359380
function getUserInstructionDeps(
360381
dockerfilePackages: DockerFilePackages,
361382
dependencies: {
@@ -369,6 +390,7 @@ function getUserInstructionDeps(
369390

370391
if (dockerfilePackage) {
371392
for (const dep of collectDeps(dependencies[dependencyName])) {
393+
// Transitive dependencies are keyed by their source segments.
372394
dockerfilePackages[dep.split("/")[0]] = { ...dockerfilePackage };
373395
}
374396
}
@@ -389,9 +411,67 @@ function collectDeps(pkg) {
389411
: [];
390412
}
391413

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

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)
486+
return Object.keys(deps)
416487
.filter((depName) => dockerfilePkgs[depName])
417488
.reduce((extractedDeps, depName) => {
418-
extractedDeps[depName] = allDeps[depName];
489+
extractedDeps[depName] = deps[depName];
419490
return extractedDeps;
420491
}, {});
421492
}
422493

423-
function annotateLayerIds(deps, dockerfilePkgs) {
494+
/**
495+
* Annotates the dependency tree with layer IDs.
496+
*
497+
* @important
498+
* mutates the provided `deps` object.
499+
*
500+
* @param deps - The dependencies of the image.
501+
* @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
502+
*/
503+
function annotateWithLayerIds(
504+
deps: { [depName: string]: types.DepTreeDep },
505+
dockerfilePkgs: DockerFilePackages | undefined,
506+
): void {
424507
if (!dockerfilePkgs) {
425508
return;
426509
}
427510

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-
};
436-
}
437-
if (pkg.dependencies) {
438-
annotateLayerIds(pkg.dependencies, dockerfilePkgs);
511+
function annotateRecursive(currentDeps: {
512+
[depName: string]: types.DepTreeDep;
513+
}) {
514+
for (const depKey of Object.keys(currentDeps)) {
515+
const node = currentDeps[depKey];
516+
const dockerfileEntry = dockerfilePkgs![depKey];
517+
518+
if (dockerfileEntry) {
519+
node.labels = {
520+
...(node.labels || {}),
521+
dockerLayerId: instructionDigest(dockerfileEntry.instruction),
522+
};
523+
524+
// Only progress down the dependency tree if the current node is a dockerfile package.
525+
if (node.dependencies) {
526+
annotateRecursive(node.dependencies);
527+
}
528+
}
439529
}
440530
}
531+
532+
annotateRecursive(deps);
441533
}

0 commit comments

Comments
 (0)