@@ -12,12 +12,7 @@ import * as types from "./types";
1212import { truncateAdditionalFacts } from "./utils" ;
1313import { PLUGIN_VERSION } from "./version" ;
1414
15- export {
16- buildResponse ,
17- expandDockerfilePackages ,
18- excludeBaseImageDeps ,
19- annotateWithLayerIds ,
20- } ;
15+ export { buildResponse } ;
2116
2217async function buildResponse (
2318 depsAnalysis : StaticAnalysis & {
@@ -31,39 +26,14 @@ async function buildResponse(
3126 options ?: Partial < types . PluginOptions > ,
3227) : Promise < types . PluginResponse > {
3328 const deps = depsAnalysis . depTree . dependencies ;
34-
35- // Expand both the Dockerfile packages and the auto-detected user instructions packages,
36- // storing the results back to the original objects.
37- if ( dockerfileAnalysis ?. dockerfilePackages ) {
38- dockerfileAnalysis . dockerfilePackages = expandDockerfilePackages (
39- dockerfileAnalysis . dockerfilePackages ,
40- deps ,
41- ) ;
42- }
43-
44- if ( depsAnalysis . autoDetectedUserInstructions ?. dockerfilePackages ) {
45- depsAnalysis . autoDetectedUserInstructions . dockerfilePackages =
46- expandDockerfilePackages (
47- depsAnalysis . autoDetectedUserInstructions . dockerfilePackages ,
48- deps ,
49- ) ;
50- }
51-
52- // Select a dockerfilePackages object to use for the annotation and exclusion of base image dependencies.
53- // Prioritize the Dockerfile packages over the auto-detected user instructions packages.
54- const dockerfilePkgs =
55- dockerfileAnalysis ?. dockerfilePackages ||
56- depsAnalysis . autoDetectedUserInstructions ?. dockerfilePackages ;
57-
29+ const dockerfilePkgs = collectDockerfilePkgs ( dockerfileAnalysis , deps ) ;
5830 const finalDeps = excludeBaseImageDeps (
5931 deps ,
6032 dockerfilePkgs ,
6133 excludeBaseImageVulns ,
6234 ) ;
63- annotateWithLayerIds ( finalDeps , dockerfilePkgs ) ;
64-
65- // Apply the filtered dependencies back to the depTree
66- depsAnalysis . depTree . dependencies = finalDeps ;
35+ /** WARNING! Mutates the depTree.dependencies! */
36+ annotateLayerIds ( finalDeps , dockerfilePkgs ) ;
6737
6838 /** This must be called after all final changes to the DependencyTree. */
6939 const depGraph = await legacy . depTreeToGraph (
@@ -227,12 +197,17 @@ async function buildResponse(
227197 autoDetectedLayers &&
228198 Object . keys ( autoDetectedLayers ) . length > 0
229199 ) {
200+ const autoDetectedPackagesWithChildren = getUserInstructionDeps (
201+ autoDetectedPackages ,
202+ deps ,
203+ ) ;
204+
230205 const autoDetectedUserInstructionsFact : facts . AutoDetectedUserInstructionsFact =
231206 {
232207 type : "autoDetectedUserInstructions" ,
233208 data : {
234209 dockerfileLayers : autoDetectedLayers ,
235- dockerfilePackages : autoDetectedPackages ! ,
210+ dockerfilePackages : autoDetectedPackagesWithChildren ! ,
236211 } ,
237212 } ;
238213 additionalFacts . push ( autoDetectedUserInstructionsFact ) ;
@@ -364,68 +339,59 @@ async function buildResponse(
364339 } ;
365340}
366341
367- /**
368- * Returns the package source name from a full dependency name.
369- *
370- * A package source refers to the top-level package name, such as "bzip2" in "bzip2/libbz2-dev".
371- *
372- * @param depName - The full dependency name.
373- * @returns The package source name.
374- */
375- function packageSource ( depName : string ) : string {
376- return depName . split ( "/" ) [ 0 ] ;
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 ) ;
377353}
378354
379- /**
380- * Expands the list of packages explicitly requested in the Dockerfile to include all transitive dependencies.
381- *
382- * The returned package map is keyed by the full dependency names. Package names extracted from the Dockerfile
383- * (typically in the form of source segments) are copied from the input map into the returned map to maintain
384- * compatibility with the CLI dockerfile-attribution logic.
385- *
386- * @param dockerfilePackages - The packages explicitly requested in a Dockerfile.
387- * @param deps - The dependencies of the image.
388- * @returns A map of packages attributed to the Dockerfile.
389- */
390- function expandDockerfilePackages (
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 (
391360 dockerfilePackages : DockerFilePackages ,
392- deps : { [ depName : string ] : types . DepTreeDep } ,
361+ dependencies : {
362+ [ depName : string ] : types . DepTreeDep ;
363+ } ,
393364) : DockerFilePackages {
394- const expandedPkgs = { ...dockerfilePackages } ;
395-
396- function collectChildPackages ( node : types . DepTreeDep , parentEntry : any ) {
397- if ( ! node . dependencies ) {
398- return ;
399- }
400- for ( const childKey of Object . keys ( node . dependencies ) ) {
401- if ( ! expandedPkgs [ childKey ] ) {
402- expandedPkgs [ childKey ] = parentEntry ;
403- collectChildPackages ( node . dependencies [ childKey ] , parentEntry ) ;
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+ }
404374 }
405375 }
406376 }
407377
408- for ( const rootKey of Object . keys ( deps ) ) {
409- const source = packageSource ( rootKey ) ;
410- const dockerfileEntry = expandedPkgs [ rootKey ] || expandedPkgs [ source ] ;
411- if ( dockerfileEntry ) {
412- // Ensure the full dependency name is in the expanded packages.
413- expandedPkgs [ rootKey ] = dockerfileEntry ;
414- collectChildPackages ( deps [ rootKey ] , dockerfileEntry ) ;
415- }
416- }
378+ return dockerfilePackages ;
379+ }
417380
418- return expandedPkgs ;
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+ : [ ] ;
419390}
420391
421- /**
422- * Excludes base image dependencies from the dependency tree if excludeBaseImageVulns is true.
423- *
424- * @param deps - The dependencies of the image.
425- * @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
426- * @param excludeBaseImageVulns - Whether to exclude base image dependencies.
427- * @returns The dependencies of the image.
428- */
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.
429395function excludeBaseImageDeps (
430396 deps : {
431397 [ depName : string ] : types . DepTreeDep ;
@@ -437,48 +403,39 @@ function excludeBaseImageDeps(
437403 return deps ;
438404 }
439405
440- return Object . keys ( deps )
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 )
441416 . filter ( ( depName ) => dockerfilePkgs [ depName ] )
442417 . reduce ( ( extractedDeps , depName ) => {
443- extractedDeps [ depName ] = deps [ depName ] ;
418+ extractedDeps [ depName ] = allDeps [ depName ] ;
444419 return extractedDeps ;
445420 } , { } ) ;
446421}
447422
448- /**
449- * Annotates the dependency tree with layer IDs. Mutates the recieved dependency tree.
450- *
451- * @param deps - The dependencies of the image.
452- * @param dockerfilePkgs - The expanded packages attributed to the Dockerfile.
453- */
454- function annotateWithLayerIds (
455- deps : { [ depName : string ] : types . DepTreeDep } ,
456- dockerfilePkgs : DockerFilePackages | undefined ,
457- ) : void {
423+ function annotateLayerIds ( deps , dockerfilePkgs ) {
458424 if ( ! dockerfilePkgs ) {
459425 return ;
460426 }
461427
462- function annotateRecursive ( currentDeps : {
463- [ depName : string ] : types . DepTreeDep ;
464- } ) {
465- for ( const depKey of Object . keys ( currentDeps ) ) {
466- const node = currentDeps [ depKey ] ;
467- const dockerfileEntry = dockerfilePkgs ! [ depKey ] ;
468-
469- if ( dockerfileEntry ) {
470- node . labels = {
471- ...( node . labels || { } ) ,
472- dockerLayerId : instructionDigest ( dockerfileEntry . instruction ) ,
473- } ;
474-
475- // Only progress down the dependency tree if the current node is a dockerfile package.
476- if ( node . dependencies ) {
477- annotateRecursive ( node . dependencies ) ;
478- }
479- }
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 ) ;
480439 }
481440 }
482-
483- annotateRecursive ( deps ) ;
484441}
0 commit comments