@@ -120,7 +120,7 @@ use std::sync::Arc;
120120use crate :: OCSF_VERSION ;
121121use crate :: enums:: StatusId ;
122122use crate :: events:: base_event:: BaseEventData ;
123- use crate :: objects:: { Container , Device , Endpoint , Image , Metadata , Product , ResourceDetails } ;
123+ use crate :: objects:: { Container , Device , Endpoint , Image , Metadata , Product } ;
124124
125125/// Which `OpenShell` component produced an event.
126126#[ derive( Debug , Clone , PartialEq , Eq ) ]
@@ -238,7 +238,8 @@ impl EventContext {
238238 . filter ( |uid| !uid. trim ( ) . is_empty ( ) )
239239 . map ( |uid| Image {
240240 uid : uid. clone ( ) ,
241- name : self . container_image . clone ( ) ,
241+ name : ( !self . container_image . trim ( ) . is_empty ( ) )
242+ . then ( || self . container_image . clone ( ) ) ,
242243 } ) ,
243244 } )
244245 }
@@ -255,25 +256,6 @@ impl EventContext {
255256 device
256257 }
257258
258- /// Identify the affected sandbox without claiming it runs on the gateway device.
259- pub ( crate ) fn add_sandbox_resource ( & self , resources : & mut Vec < ResourceDetails > ) {
260- if self . sandbox_id . is_empty ( ) || !matches ! ( self . origin, EventOrigin :: Gateway { .. } ) {
261- return ;
262- }
263- let existing = resources. iter ( ) . position ( |r| {
264- r. type_label . as_deref ( ) == Some ( "sandbox" ) && r. uid . as_deref ( ) == Some ( & self . sandbox_id )
265- } ) ;
266- let mut resource = existing. map_or_else (
267- || ResourceDetails :: new ( "sandbox" , & self . sandbox_id ) ,
268- |index| resources. remove ( index) ,
269- ) ;
270- if resource. name . is_none ( ) && !self . sandbox_name . is_empty ( ) {
271- resource. name = Some ( self . sandbox_name . clone ( ) ) ;
272- }
273- // Keep the context sandbox first when an event also names other targets.
274- resources. insert ( 0 , resource) ;
275- }
276-
277259 /// Build the `proxy_endpoint` object for the Network Proxy profile.
278260 #[ must_use]
279261 pub fn proxy_endpoint ( & self ) -> Endpoint {
@@ -295,14 +277,21 @@ impl EventContext {
295277 base. set_message ( m) ;
296278 }
297279 base. set_device ( self . device ( ) ) ;
298- // These classes have no resources attribute. Keep gateway attribution
299- // in the standard extension object rather than on the gateway device.
300- if matches ! ( self . origin, EventOrigin :: Gateway { .. } )
301- && !self . sandbox_id . is_empty ( )
302- && !matches ! ( base. class_uid, 6003 | 2004 )
303- {
280+ // Only trusted context identity may route an event to a sandbox.
281+ // Resources can name request-supplied targets, including denied ones.
282+ if matches ! ( self . origin, EventOrigin :: Gateway { .. } ) && !self . sandbox_id . is_empty ( ) {
304283 base. add_unmapped ( "sandbox_id" , self . sandbox_id . clone ( ) ) ;
305- base. add_unmapped ( "sandbox_name" , self . sandbox_name . clone ( ) ) ;
284+ if !self . sandbox_name . is_empty ( ) {
285+ base. add_unmapped ( "sandbox_name" , self . sandbox_name . clone ( ) ) ;
286+ }
287+ }
288+ if self
289+ . container_image_uid
290+ . as_ref ( )
291+ . is_none_or ( |uid| uid. trim ( ) . is_empty ( ) )
292+ && !self . container_image . trim ( ) . is_empty ( )
293+ {
294+ base. add_unmapped ( "container_image" , self . container_image . clone ( ) ) ;
306295 }
307296 }
308297}
@@ -451,6 +440,54 @@ pub(crate) fn test_gateway_context() -> EventContext {
451440mod tests {
452441 use super :: * ;
453442
443+ #[ test]
444+ fn image_identity_preserves_references_and_omits_missing_names ( ) {
445+ for uid in [ None , Some ( "" ) , Some ( " " ) , Some ( "sha256:resolved-workload" ) ] {
446+ for reference in [ "" , " " , "registry.example/workload:latest" ] {
447+ let mut ctx = test_sandbox_context ( ) ;
448+ ctx. container_image_uid = uid. map ( str:: to_string) ;
449+ ctx. container_image = reference. into ( ) ;
450+ let resolved = uid. filter ( |uid| !uid. trim ( ) . is_empty ( ) ) ;
451+ for ( class, event) in sample_events ( & ctx) {
452+ let json = event. to_json ( ) . unwrap ( ) ;
453+ openshell_ocsf_validate:: validate_event ( & json)
454+ . unwrap_or_else ( |error| panic ! ( "{class}: {error}" ) ) ;
455+ let image = & json[ "device" ] [ "container" ] [ "image" ] ;
456+ if class != "authentication" && resolved. is_some ( ) {
457+ assert_eq ! ( image[ "uid" ] . as_str( ) , resolved) ;
458+ assert_eq ! (
459+ image. get( "name" ) . and_then( serde_json:: Value :: as_str) ,
460+ ( !reference. trim( ) . is_empty( ) ) . then_some( reference)
461+ ) ;
462+ } else {
463+ assert ! ( image. is_null( ) ) ;
464+ }
465+ assert_eq ! (
466+ json[ "unmapped" ] [ "container_image" ] . as_str( ) ,
467+ ( resolved. is_none( ) && !reference. trim( ) . is_empty( ) ) . then_some( reference)
468+ ) ;
469+ }
470+ }
471+ }
472+ }
473+
474+ #[ test]
475+ fn every_gateway_class_routes_only_by_context_identity ( ) {
476+ for sandbox_id in [ "" , "sb-context" ] {
477+ let mut ctx = test_gateway_context ( ) ;
478+ ctx. sandbox_id = sandbox_id. into ( ) ;
479+ for ( class, event) in sample_events ( & ctx) {
480+ let json = event. to_json ( ) . unwrap ( ) ;
481+ assert_eq ! (
482+ event. sandbox_id( ) ,
483+ ( !sandbox_id. is_empty( ) ) . then_some( sandbox_id) ,
484+ "{class}"
485+ ) ;
486+ assert ! ( json[ "unmapped" ] . get( "sandbox_name" ) . is_none( ) , "{class}" ) ;
487+ }
488+ }
489+ }
490+
454491 #[ test]
455492 fn resolved_container_image_matches_schema ( ) {
456493 let mut ctx = test_sandbox_context ( ) ;
0 commit comments