@@ -12,7 +12,12 @@ import * as types from "./types";
1212import { truncateAdditionalFacts } from "./utils" ;
1313import { PLUGIN_VERSION } from "./version" ;
1414
15- export { buildResponse } ;
15+ export {
16+ buildResponse ,
17+ annotateWithLayerIds ,
18+ expandDockerfilePackages ,
19+ excludeBaseImageDeps ,
20+ } ;
1621
1722async 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+ */
352373function 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+ */
388468function 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