@@ -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 (
@@ -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+ */
359380function 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+ */
395475function 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