@@ -26,14 +26,19 @@ async function buildResponse(
2626 options ?: Partial < types . PluginOptions > ,
2727) : Promise < types . PluginResponse > {
2828 const deps = depsAnalysis . depTree . dependencies ;
29- const dockerfilePkgs = collectDockerfilePkgs ( dockerfileAnalysis , deps ) ;
29+ const dockerfilePkgs = dockerfileAnalysis ?. dockerfilePackages ?? { } ;
30+
31+ /** WARNING! Mutates the depTree.dependencies! */
32+ annotateLayerIds ( deps , dockerfilePkgs ) ;
33+
3034 const finalDeps = excludeBaseImageDeps (
3135 deps ,
3236 dockerfilePkgs ,
3337 excludeBaseImageVulns ,
3438 ) ;
35- /** WARNING! Mutates the depTree.dependencies! */
36- annotateLayerIds ( finalDeps , dockerfilePkgs ) ;
39+
40+ // Apply the filtered dependencies back to the depTree
41+ depsAnalysis . depTree . dependencies = finalDeps ;
3742
3843 /** This must be called after all final changes to the DependencyTree. */
3944 const depGraph = await legacy . depTreeToGraph (
@@ -190,7 +195,7 @@ async function buildResponse(
190195 autoDetectedLayers &&
191196 Object . keys ( autoDetectedLayers ) . length > 0
192197 ) {
193- const autoDetectedPackagesWithChildren = getUserInstructionDeps (
198+ const autoDetectedPackagesWithChildren = mapDepTreeToDockerfilePackages (
194199 autoDetectedPackages ,
195200 deps ,
196201 ) ;
@@ -332,59 +337,59 @@ async function buildResponse(
332337 } ;
333338}
334339
335- function collectDockerfilePkgs (
336- dockerAnalysis : DockerFileAnalysis | undefined ,
337- deps : {
338- [ depName : string ] : types . DepTreeDep ;
339- } ,
340- ) {
341- if ( ! dockerAnalysis ) {
342- return ;
343- }
340+ // Returns the package source name from a dependency key. A package source refers
341+ // to the top-level Linux package name, such as "bzip2" in "bzip2/libbz2-dev".
342+ function packageSource ( depKey : string ) : string {
343+ return depKey . split ( "/" ) [ 0 ] ;
344+ }
344345
345- return getUserInstructionDeps ( dockerAnalysis . dockerfilePackages , deps ) ;
346+ function collectTransitiveDepKeys ( pkg : types . DepTreeDep ) : string [ ] {
347+ if ( ! pkg . dependencies || Object . keys ( pkg . dependencies ) . length === 0 ) {
348+ return [ ] ;
349+ }
350+ const keys = Object . keys ( pkg . dependencies ) ;
351+ const nested : string [ ] = [ ] ;
352+ for ( const key of keys ) {
353+ const childKeys = collectTransitiveDepKeys ( pkg . dependencies ! [ key ] ) ;
354+ for ( const childKey of childKeys ) {
355+ nested . push ( childKey ) ;
356+ }
357+ }
358+ return keys . concat ( nested ) ;
346359}
347360
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.
352- function getUserInstructionDeps (
353- dockerfilePackages : DockerFilePackages ,
354- dependencies : {
355- [ depName : string ] : types . DepTreeDep ;
356- } ,
361+ // Maps each dependency key (and its transitives) that matches a dockerfile-
362+ // installed package to that package's instruction.
363+ export function mapDepTreeToDockerfilePackages (
364+ dockerfilePkgs : DockerFilePackages ,
365+ deps : { [ depName : string ] : types . DepTreeDep } ,
357366) : DockerFilePackages {
358- for ( const dependencyName in dependencies ) {
359- if ( dependencies . hasOwnProperty ( dependencyName ) ) {
360- const sourceOrName = dependencyName . split ( "/" ) [ 0 ] ;
361- const dockerfilePackage = dockerfilePackages [ sourceOrName ] ;
362-
363- if ( dockerfilePackage ) {
364- for ( const dep of collectDeps ( dependencies [ dependencyName ] ) ) {
365- dockerfilePackages [ dep . split ( "/" ) [ 0 ] ] = { ...dockerfilePackage } ;
366- }
367- }
368- }
367+ if ( ! dockerfilePkgs ) {
368+ return { } ;
369369 }
370370
371- return dockerfilePackages ;
372- }
371+ for ( const rootKey of Object . keys ( deps ) ) {
372+ const source = packageSource ( rootKey ) ;
373+ const instruction = dockerfilePkgs [ rootKey ] || dockerfilePkgs [ source ] ;
374+ if ( ! instruction ) {
375+ continue ;
376+ }
377+
378+ // Ensure the instruction data is stored under the key that matches the
379+ // dependency tree.
380+ dockerfilePkgs [ rootKey ] = instruction ;
381+
382+ const transitiveKeys = collectTransitiveDepKeys ( deps [ rootKey ] ) ;
383+ for ( const key of transitiveKeys ) {
384+ dockerfilePkgs [ key ] = instruction ;
385+ }
386+ }
373387
374- function collectDeps ( pkg ) {
375- // ES5 doesn't have Object.values, so replace with Object.keys() and map()
376- return pkg . dependencies
377- ? Object . keys ( pkg . dependencies )
378- . map ( ( name ) => pkg . dependencies [ name ] )
379- . reduce ( ( allDeps , pkg ) => {
380- return [ ...allDeps , ...collectDeps ( pkg ) ] ;
381- } , Object . keys ( pkg . dependencies ) )
382- : [ ] ;
388+ return dockerfilePkgs ;
383389}
384390
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.
391+ // If excludeBaseImageVulns is true, only retain dependencies that are
392+ // dockerfile-introduced, as defined by dockerfilePkgs.
388393function excludeBaseImageDeps (
389394 deps : {
390395 [ depName : string ] : types . DepTreeDep ;
@@ -396,39 +401,62 @@ function excludeBaseImageDeps(
396401 return deps ;
397402 }
398403
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 )
409- . filter ( ( depName ) => dockerfilePkgs [ depName ] )
404+ return Object . keys ( deps )
405+ . filter (
406+ ( depName ) =>
407+ dockerfilePkgs [ depName ] || dockerfilePkgs [ packageSource ( depName ) ] ,
408+ )
410409 . reduce ( ( extractedDeps , depName ) => {
411- extractedDeps [ depName ] = allDeps [ depName ] ;
410+ extractedDeps [ depName ] = deps [ depName ] ;
412411 return extractedDeps ;
413412 } , { } ) ;
414413}
415414
416- function annotateLayerIds ( deps , dockerfilePkgs ) {
415+ // Annotates dockerfile-introduced dependencies and sub-dependencies with the
416+ // instruction ID. A dependency is identified as dockerfile-introduced if the
417+ // dependency key or source was found in a dockerfile installation instruction.
418+ function annotateLayerIds (
419+ deps : { [ depName : string ] : types . DepTreeDep } ,
420+ dockerfilePkgs : DockerFilePackages | undefined ,
421+ ) : void {
417422 if ( ! dockerfilePkgs ) {
418423 return ;
419424 }
420425
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- } ;
426+ for ( const rootKey of Object . keys ( deps ) ) {
427+ const source = packageSource ( rootKey ) ;
428+ const dockerfileEntry = dockerfilePkgs [ rootKey ] || dockerfilePkgs [ source ] ;
429+ if ( ! dockerfileEntry ) {
430+ continue ;
431+ }
432+
433+ const rootNode = deps [ rootKey ] ;
434+ const layerId = instructionDigest ( dockerfileEntry . instruction ) ;
435+ rootNode . labels = {
436+ ...( rootNode . labels || { } ) ,
437+ dockerLayerId : layerId ,
438+ } ;
439+ if (
440+ rootNode . dependencies &&
441+ Object . keys ( rootNode . dependencies ) . length > 0
442+ ) {
443+ annotateSubtreeWithLayerId ( rootNode . dependencies , layerId ) ;
429444 }
430- if ( pkg . dependencies ) {
431- annotateLayerIds ( pkg . dependencies , dockerfilePkgs ) ;
445+ }
446+ }
447+
448+ function annotateSubtreeWithLayerId (
449+ deps : { [ depName : string ] : types . DepTreeDep } ,
450+ dockerLayerId : string ,
451+ ) : void {
452+ for ( const depKey of Object . keys ( deps ) ) {
453+ const node = deps [ depKey ] ;
454+ node . labels = {
455+ ...( node . labels || { } ) ,
456+ dockerLayerId,
457+ } ;
458+ if ( node . dependencies && Object . keys ( node . dependencies ) . length > 0 ) {
459+ annotateSubtreeWithLayerId ( node . dependencies , dockerLayerId ) ;
432460 }
433461 }
434462}
0 commit comments