@@ -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 (
@@ -197,7 +202,7 @@ async function buildResponse(
197202 autoDetectedLayers &&
198203 Object . keys ( autoDetectedLayers ) . length > 0
199204 ) {
200- const autoDetectedPackagesWithChildren = getUserInstructionDeps (
205+ const autoDetectedPackagesWithChildren = mapDepTreeToDockerfilePackages (
201206 autoDetectedPackages ,
202207 deps ,
203208 ) ;
@@ -339,59 +344,59 @@ async function buildResponse(
339344 } ;
340345}
341346
342- function collectDockerfilePkgs (
343- dockerAnalysis : DockerFileAnalysis | undefined ,
344- deps : {
345- [ depName : string ] : types . DepTreeDep ;
346- } ,
347- ) {
348- if ( ! dockerAnalysis ) {
349- return ;
350- }
347+ // Returns the package source name from a dependency key. A package source refers
348+ // to the top-level Linux package name, such as "bzip2" in "bzip2/libbz2-dev".
349+ function packageSource ( depKey : string ) : string {
350+ return depKey . split ( "/" ) [ 0 ] ;
351+ }
351352
352- return getUserInstructionDeps ( dockerAnalysis . dockerfilePackages , deps ) ;
353+ function collectTransitiveDepKeys ( pkg : types . DepTreeDep ) : string [ ] {
354+ if ( ! pkg . dependencies || Object . keys ( pkg . dependencies ) . length === 0 ) {
355+ return [ ] ;
356+ }
357+ const keys = Object . keys ( pkg . dependencies ) ;
358+ const nested : string [ ] = [ ] ;
359+ for ( const key of keys ) {
360+ const childKeys = collectTransitiveDepKeys ( pkg . dependencies ! [ key ] ) ;
361+ for ( const childKey of childKeys ) {
362+ nested . push ( childKey ) ;
363+ }
364+ }
365+ return keys . concat ( nested ) ;
353366}
354367
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 (
360- dockerfilePackages : DockerFilePackages ,
361- dependencies : {
362- [ depName : string ] : types . DepTreeDep ;
363- } ,
368+ // Maps each dependency key (and its transitives) that matches a dockerfile-
369+ // installed package to that package's instruction.
370+ export function mapDepTreeToDockerfilePackages (
371+ dockerfilePkgs : DockerFilePackages ,
372+ deps : { [ depName : string ] : types . DepTreeDep } ,
364373) : DockerFilePackages {
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- }
374- }
375- }
374+ if ( ! dockerfilePkgs ) {
375+ return { } ;
376376 }
377377
378- return dockerfilePackages ;
379- }
378+ for ( const rootKey of Object . keys ( deps ) ) {
379+ const source = packageSource ( rootKey ) ;
380+ const instruction = dockerfilePkgs [ rootKey ] || dockerfilePkgs [ source ] ;
381+ if ( ! instruction ) {
382+ continue ;
383+ }
384+
385+ // Ensure the instruction data is stored under the key that matches the
386+ // dependency tree.
387+ dockerfilePkgs [ rootKey ] = instruction ;
388+
389+ const transitiveKeys = collectTransitiveDepKeys ( deps [ rootKey ] ) ;
390+ for ( const key of transitiveKeys ) {
391+ dockerfilePkgs [ key ] = instruction ;
392+ }
393+ }
380394
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- : [ ] ;
395+ return dockerfilePkgs ;
390396}
391397
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.
398+ // If excludeBaseImageVulns is true, only retain dependencies that are
399+ // dockerfile-introduced, as defined by dockerfilePkgs.
395400function excludeBaseImageDeps (
396401 deps : {
397402 [ depName : string ] : types . DepTreeDep ;
@@ -403,39 +408,62 @@ function excludeBaseImageDeps(
403408 return deps ;
404409 }
405410
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 )
416- . filter ( ( depName ) => dockerfilePkgs [ depName ] )
411+ return Object . keys ( deps )
412+ . filter (
413+ ( depName ) =>
414+ dockerfilePkgs [ depName ] || dockerfilePkgs [ packageSource ( depName ) ] ,
415+ )
417416 . reduce ( ( extractedDeps , depName ) => {
418- extractedDeps [ depName ] = allDeps [ depName ] ;
417+ extractedDeps [ depName ] = deps [ depName ] ;
419418 return extractedDeps ;
420419 } , { } ) ;
421420}
422421
423- function annotateLayerIds ( deps , dockerfilePkgs ) {
422+ // Annotates dockerfile-introduced dependencies and sub-dependencies with the
423+ // instruction ID. A dependency is identified as dockerfile-introduced if the
424+ // dependency key or source was found in a dockerfile installation instruction.
425+ function annotateLayerIds (
426+ deps : { [ depName : string ] : types . DepTreeDep } ,
427+ dockerfilePkgs : DockerFilePackages | undefined ,
428+ ) : void {
424429 if ( ! dockerfilePkgs ) {
425430 return ;
426431 }
427432
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- } ;
433+ for ( const rootKey of Object . keys ( deps ) ) {
434+ const source = packageSource ( rootKey ) ;
435+ const dockerfileEntry = dockerfilePkgs [ rootKey ] || dockerfilePkgs [ source ] ;
436+ if ( ! dockerfileEntry ) {
437+ continue ;
438+ }
439+
440+ const rootNode = deps [ rootKey ] ;
441+ const layerId = instructionDigest ( dockerfileEntry . instruction ) ;
442+ rootNode . labels = {
443+ ...( rootNode . labels || { } ) ,
444+ dockerLayerId : layerId ,
445+ } ;
446+ if (
447+ rootNode . dependencies &&
448+ Object . keys ( rootNode . dependencies ) . length > 0
449+ ) {
450+ annotateSubtreeWithLayerId ( rootNode . dependencies , layerId ) ;
436451 }
437- if ( pkg . dependencies ) {
438- annotateLayerIds ( pkg . dependencies , dockerfilePkgs ) ;
452+ }
453+ }
454+
455+ function annotateSubtreeWithLayerId (
456+ deps : { [ depName : string ] : types . DepTreeDep } ,
457+ dockerLayerId : string ,
458+ ) : void {
459+ for ( const depKey of Object . keys ( deps ) ) {
460+ const node = deps [ depKey ] ;
461+ node . labels = {
462+ ...( node . labels || { } ) ,
463+ dockerLayerId,
464+ } ;
465+ if ( node . dependencies && Object . keys ( node . dependencies ) . length > 0 ) {
466+ annotateSubtreeWithLayerId ( node . dependencies , dockerLayerId ) ;
439467 }
440468 }
441469}
0 commit comments