@@ -52,8 +52,8 @@ type processor struct {
5252 workloadmetaStore workloadmeta.Component
5353 containerFilter workloadfilter.FilterBundle
5454 tagger tagger.Component
55- imageRepoDigests map [string ]string // Map where keys are image repo digest and values are image ID
56- imageUsers map [string ]map [ string ] struct {} // Map where keys are image repo digest and values are set of container IDs
55+ imageRepoDigests map [string ]string // Map where keys are image repo digest and values are image ID
56+ imagesInUse map [string ]struct {} // Set of image IDs the back end was last told are in use
5757 sbomScanner * sbomscanner.Scanner
5858 contImageSBOM bool
5959 hostSBOM bool
@@ -102,7 +102,7 @@ func newProcessor(workloadmetaStore workloadmeta.Component, filterStore workload
102102 containerFilter : filterStore .GetContainerSBOMFilters (),
103103 tagger : tagger ,
104104 imageRepoDigests : make (map [string ]string ),
105- imageUsers : make (map [ string ] map [string ]struct {}),
105+ imagesInUse : make (map [string ]struct {}),
106106 sbomScanner : sbomScanner ,
107107 contImageSBOM : contImageSBOM ,
108108 hostSBOM : hostSBOM ,
@@ -118,17 +118,24 @@ func isProcfsSBOMEnabled(cfg config.Component) bool {
118118}
119119
120120func (p * processor ) processContainerImagesEvents (evBundle workloadmeta.EventBundle ) {
121+ // The store already reflects the events in this bundle, so ask it which
122+ // images are in use rather than tracking container events ourselves. Ask
123+ // before acknowledging: the store hands the next bundle to the next
124+ // subscriber as soon as this one acknowledges, and would then answer for a
125+ // later moment than the bundle being processed describes.
126+ running := runningImages (p .workloadmetaStore )
127+
121128 evBundle .Acknowledge ()
122129
123130 log .Tracef ("Processing %d events" , len (evBundle .Events ))
124131
125- // Separate events by kind and type so we can process them in an order that
126- // keeps imageUsers accurate when SBOMs are computed.
132+ // Separate events by kind and type. Image events are handled first so that
133+ // imageRepoDigests is up to date when the identifiers the containers use
134+ // are resolved below.
127135 var (
128- imageSetEvents []workloadmeta.Event
129- imageUnsetEvents []workloadmeta.Event
130- containerSetEvents []workloadmeta.Event
131- containerUnsetEvents []workloadmeta.Event
136+ imageSetEvents []workloadmeta.Event
137+ imageUnsetEvents []workloadmeta.Event
138+ containerSetEvents []workloadmeta.Event
132139 )
133140
134141 for _ , event := range evBundle .Events {
@@ -142,42 +149,49 @@ func (p *processor) processContainerImagesEvents(evBundle workloadmeta.EventBund
142149 case workloadmeta .KindContainer :
143150 if event .Type == workloadmeta .EventTypeSet {
144151 containerSetEvents = append (containerSetEvents , event )
145- } else {
146- containerUnsetEvents = append (containerUnsetEvents , event )
147152 }
148153 }
149154 }
150155
151- // 1. Unregister removed containers first so imageUsers is up to date before
152- // we compute inUse below. Processing image Set events before these removals
153- // would cause false-positive inUse=true on the emitted SBOM.
154- for _ , event := range containerUnsetEvents {
155- p .unregisterContainer (event .Entity .(* workloadmeta.Container ))
156- }
156+ // Images reported in this bundle, so that an image and the container that
157+ // just started it don't each produce an SBOM.
158+ reported := make (map [string ]struct {}, len (imageSetEvents ))
157159
158- // 2. Unregister removed images.
159160 for _ , event := range imageUnsetEvents {
160161 p .unregisterImage (event .Entity .(* workloadmeta.ContainerImageMetadata ))
161162 // Let the SBOM expire on back-end side
162163 }
163164
164- // 3. Register updated images and emit SBOMs; inUse now reflects the
165- // current set of running containers.
166165 for _ , event := range imageSetEvents {
167- filterableContainerImage := workloadfilter .CreateContainerImage (event .Entity .(* workloadmeta.ContainerImageMetadata ).Name )
166+ img := event .Entity .(* workloadmeta.ContainerImageMetadata )
167+
168+ filterableContainerImage := workloadfilter .CreateContainerImage (img .Name )
168169 if p .containerFilter .IsExcluded (filterableContainerImage ) {
169170 continue
170171 }
171172
172- p .registerImage (event .Entity .(* workloadmeta.ContainerImageMetadata ))
173- p .processImageSBOM (event .Entity .(* workloadmeta.ContainerImageMetadata ))
173+ p .registerImage (img )
174+ p .processImageSBOM (img , running )
175+ reported [img .ID ] = struct {}{}
176+ }
177+
178+ // Report images that gained their first running container, so that the
179+ // back end learns about them without waiting for the periodic refresh.
180+ // Containers name the same image in more than one way, so compare resolved
181+ // image IDs rather than the identifiers they use.
182+ imagesInUse := make (map [string ]struct {}, len (running ))
183+ for id := range running {
184+ imgID := p .resolveImageID (id )
185+ imagesInUse [imgID ] = struct {}{}
186+
187+ if _ , found := p .imagesInUse [imgID ]; ! found {
188+ p .reportImage (imgID , running , reported )
189+ }
174190 }
191+ p .imagesInUse = imagesInUse
175192
176- // 4. Register new/updated containers. registerContainer may emit a second
177- // SBOM for an image that just gained its first running container.
178193 for _ , event := range containerSetEvents {
179194 container := event .Entity .(* workloadmeta.Container )
180- p .registerContainer (container )
181195
182196 filterableContainer := workloadmetafilter .CreateContainer (container , nil )
183197 if p .containerFilter .IsExcluded (filterableContainer ) {
@@ -200,52 +214,71 @@ func (p *processor) registerImage(img *workloadmeta.ContainerImageMetadata) {
200214
201215func (p * processor ) unregisterImage (img * workloadmeta.ContainerImageMetadata ) {
202216 for _ , repoDigest := range img .RepoDigests {
203- delete (p .imageUsers , repoDigest )
204217 if p .imageRepoDigests [repoDigest ] == img .ID {
205218 delete (p .imageRepoDigests , repoDigest )
206219 }
207220 }
208221}
209222
210- func (p * processor ) registerContainer (ctr * workloadmeta.Container ) {
211- imgID := ctr .Image .ID
212- ctrID := ctr .ID
223+ // runningImages returns the identifiers of the images that have at least one
224+ // running container. Depending on the runtime and on which workloadmeta
225+ // sources describe it, a container names its image either by image ID or by
226+ // repo digest, so the identifiers are returned as the containers spell them.
227+ func runningImages (store workloadmeta.Component ) map [string ]struct {} {
228+ containers := store .ListContainersWithFilter (workloadmeta .GetRunningContainers )
229+
230+ images := make (map [string ]struct {}, len (containers ))
231+ for _ , ctr := range containers {
232+ if ctr .Image .ID != "" {
233+ images [ctr .Image .ID ] = struct {}{}
234+ }
235+ }
213236
214- if ! ctr .State .Running {
215- // Container is no longer running. Remove it from imageUsers so that a
216- // subsequent SBOM computation does not incorrectly set inUse=true for
217- // an image that has no running containers.
218- p .unregisterContainer (ctr )
219- return
237+ return images
238+ }
239+
240+ // imageInUse reports whether one of the running images is img, named either by
241+ // its ID or by one of its repo digests.
242+ func imageInUse (img * workloadmeta.ContainerImageMetadata , running map [string ]struct {}) bool {
243+ if _ , found := running [img .ID ]; found {
244+ return true
220245 }
221246
222- if _ , found := p .imageUsers [imgID ]; found {
223- p.imageUsers [imgID ][ctrID ] = struct {}{}
224- } else {
225- p .imageUsers [imgID ] = map [string ]struct {}{
226- ctrID : {},
247+ for _ , repoDigest := range img .RepoDigests {
248+ if _ , found := running [repoDigest ]; found {
249+ return true
227250 }
251+ }
228252
229- if realImgID , found := p .imageRepoDigests [imgID ]; found {
230- imgID = realImgID
231- }
253+ return false
254+ }
232255
233- if img , err := p .workloadmetaStore .GetImage (imgID ); err != nil {
234- log .Infof ("Couldn’t find image %s in workloadmeta whereas it’s used by container %s: %v" , imgID , ctrID , err )
235- } else {
236- p .processImageSBOM (img )
237- }
256+ // resolveImageID maps the identifier a container uses to name its image to the
257+ // ID of the corresponding image entity. Identifiers that are already image IDs
258+ // are returned unchanged.
259+ func (p * processor ) resolveImageID (id string ) string {
260+ if imgID , found := p .imageRepoDigests [id ]; found {
261+ return imgID
238262 }
263+
264+ return id
239265}
240266
241- func (p * processor ) unregisterContainer (ctr * workloadmeta.Container ) {
242- imgID := ctr .Image .ID
243- ctrID := ctr .ID
267+ // reportImage emits the SBOM of the image named by imgID, unless it has already
268+ // been reported for the event bundle being processed.
269+ func (p * processor ) reportImage (imgID string , running , reported map [string ]struct {}) {
270+ if _ , found := reported [imgID ]; found {
271+ return
272+ }
244273
245- delete (p .imageUsers [imgID ], ctrID )
246- if len (p .imageUsers [imgID ]) == 0 {
247- delete (p .imageUsers , imgID )
274+ img , err := p .workloadmetaStore .GetImage (imgID )
275+ if err != nil {
276+ log .Infof ("Couldn't find image %s in workloadmeta although a container runs it: %v" , imgID , err )
277+ return
248278 }
279+
280+ p .processImageSBOM (img , running )
281+ reported [imgID ] = struct {}{}
249282}
250283
251284func (p * processor ) processHostScanResult (result sbom.ScanResult ) {
@@ -362,7 +395,7 @@ func (p *processor) processProcfsScanResult(result sbom.ScanResult) {
362395 p .queue <- sbom
363396}
364397
365- func (p * processor ) processImageSBOM (img * workloadmeta.ContainerImageMetadata ) {
398+ func (p * processor ) processImageSBOM (img * workloadmeta.ContainerImageMetadata , running map [ string ] struct {} ) {
366399 if ! p .contImageSBOM {
367400 return
368401 }
@@ -398,17 +431,14 @@ func (p *processor) processImageSBOM(img *workloadmeta.ContainerImageMetadata) {
398431 repos [repoName ] = struct {}{}
399432 }
400433
401- inUse := false
402- for _ , repoDigest := range img .RepoDigests {
403- if _ , found := p .imageUsers [repoDigest ]; found {
404- inUse = true
405- break
406- }
407- }
408- // Fallback for runtimes (e.g. containerd) where ctr.Image.ID is the image
409- // config digest rather than a repo digest, so imageUsers is keyed by img.ID.
434+ inUse := imageInUse (img , running )
410435 if ! inUse {
411- _ , inUse = p .imageUsers [img .ID ]
436+ // A periodic refresh reaches this with no event bundle behind it, so
437+ // forget the image here rather than only when a bundle rebuilds the
438+ // set. Otherwise the back end is told the image is not in use while
439+ // the set still says it is, and a container starting it again is
440+ // taken for one that changes nothing and goes unreported.
441+ delete (p .imagesInUse , img .ID )
412442 }
413443
414444 cyclosbom , err := sbomutil .UncompressSBOM (img .SBOM )
0 commit comments