From 5d171ac753c47cfe1a9bf4a922cfc77c278a3f46 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Wed, 28 Oct 2026 21:44:21 +0000 Subject: [PATCH] fix(ocsf): move sandbox container to device.container Signed-off-by: Eric Curtin --- .../src/builders/api_activity.rs | 2 +- .../src/builders/authentication.rs | 2 +- crates/openshell-ocsf/src/builders/finding.rs | 2 +- crates/openshell-ocsf/src/builders/mod.rs | 14 ++--- crates/openshell-ocsf/src/builders/network.rs | 2 +- .../openshell-ocsf/src/events/base_event.rs | 15 ++---- crates/openshell-ocsf/src/format/downgrade.rs | 20 ++++--- crates/openshell-ocsf/src/lib.rs | 4 +- .../openshell-ocsf/src/objects/container.rs | 19 +------ crates/openshell-ocsf/src/objects/device.rs | 36 ++++++++----- crates/openshell-ocsf/src/objects/mod.rs | 2 +- .../openshell-ocsf/src/validation/schema.rs | 52 ++++++------------- crates/openshell-ocsf/tests/event_identity.rs | 15 +++--- .../openshell-ocsf/tests/gateway_context.rs | 6 +-- crates/openshell-otel/src/ocsf.rs | 5 +- crates/openshell-server/src/grpc/policy.rs | 5 +- crates/openshell-server/src/ocsf_log.rs | 2 +- crates/openshell-server/src/tracing_bus.rs | 3 +- docs/observability/ocsf-json-export.mdx | 14 ++--- e2e/rust/tests/ocsf_console.rs | 2 +- 20 files changed, 96 insertions(+), 126 deletions(-) diff --git a/crates/openshell-ocsf/src/builders/api_activity.rs b/crates/openshell-ocsf/src/builders/api_activity.rs index 4740b05053..1c92abc7fa 100644 --- a/crates/openshell-ocsf/src/builders/api_activity.rs +++ b/crates/openshell-ocsf/src/builders/api_activity.rs @@ -332,7 +332,7 @@ mod tests { json["metadata"]["profiles"], serde_json::json!(["host", "security_control"]) ); - assert!(json.get("container").is_none()); + assert!(json["device"].get("container").is_none()); } #[test] diff --git a/crates/openshell-ocsf/src/builders/authentication.rs b/crates/openshell-ocsf/src/builders/authentication.rs index 492bdf65b7..32b2b8c757 100644 --- a/crates/openshell-ocsf/src/builders/authentication.rs +++ b/crates/openshell-ocsf/src/builders/authentication.rs @@ -234,7 +234,7 @@ mod tests { json["metadata"]["profiles"], serde_json::json!(["security_control", "host"]) ); - assert!(json.get("container").is_none()); + assert!(json["device"].get("container").is_none()); assert!(json["metadata"].get("correlation_uid").is_none()); } diff --git a/crates/openshell-ocsf/src/builders/finding.rs b/crates/openshell-ocsf/src/builders/finding.rs index ccb45d29a8..182bbf277d 100644 --- a/crates/openshell-ocsf/src/builders/finding.rs +++ b/crates/openshell-ocsf/src/builders/finding.rs @@ -214,7 +214,7 @@ mod tests { ); assert_eq!(json["metadata"]["profiles"], profiles, "{sandbox_id:?}"); assert_eq!( - json.get("container").is_some(), + json["device"].get("container").is_some(), !sandbox_id.is_empty(), "{sandbox_id:?}" ); diff --git a/crates/openshell-ocsf/src/builders/mod.rs b/crates/openshell-ocsf/src/builders/mod.rs index 76acaba842..c7167ba73e 100644 --- a/crates/openshell-ocsf/src/builders/mod.rs +++ b/crates/openshell-ocsf/src/builders/mod.rs @@ -119,7 +119,7 @@ use std::sync::Arc; use crate::OCSF_VERSION; use crate::enums::StatusId; use crate::events::base_event::BaseEventData; -use crate::objects::{Container, Device, Endpoint, Image, Metadata, Product}; +use crate::objects::{Container, Device, Endpoint, Metadata, Product}; /// Which `OpenShell` component produced an event. #[derive(Debug, Clone, PartialEq, Eq)] @@ -143,7 +143,7 @@ pub struct EventContext { pub sandbox_id: String, /// Sandbox display name. pub sandbox_name: String, - /// Container image reference. + /// Container image reference. Not emitted: OCSF requires an image `uid`. pub container_image: String, /// Device hostname. pub hostname: String, @@ -224,9 +224,6 @@ impl EventContext { Some(Container { name: self.sandbox_name.clone(), uid: Some(self.sandbox_id.clone()), - image: (!self.container_image.is_empty()).then(|| Image { - name: self.container_image.clone(), - }), }) } @@ -247,7 +244,7 @@ impl EventContext { } /// Apply the fields common to every builder's `build()` method onto `base`: - /// optional status, optional message, device info, and container info. + /// optional status, optional message, and device info with its container. pub fn apply_common_fields( &self, base: &mut BaseEventData, @@ -260,10 +257,7 @@ impl EventContext { if let Some(m) = message { base.set_message(m); } - base.set_device(self.device()); - if let Some(container) = self.container() { - base.set_container(container); - } + base.set_device(self.device().with_container(self.container())); } } diff --git a/crates/openshell-ocsf/src/builders/network.rs b/crates/openshell-ocsf/src/builders/network.rs index d164384fd2..086102f3cf 100644 --- a/crates/openshell-ocsf/src/builders/network.rs +++ b/crates/openshell-ocsf/src/builders/network.rs @@ -331,7 +331,7 @@ mod tests { assert_eq!(json["dst_endpoint"]["domain"], "api.example.com"); assert_eq!(json["actor"]["process"]["name"], "python3"); assert_eq!(json["firewall_rule"]["name"], "default-egress"); - assert_eq!(json["container"]["name"], "my-sandbox"); + assert_eq!(json["device"]["container"]["name"], "my-sandbox"); assert_eq!(json["device"]["hostname"], "sandbox-abc123"); assert_eq!(json["is_src_dst_assignment_known"], true); } diff --git a/crates/openshell-ocsf/src/events/base_event.rs b/crates/openshell-ocsf/src/events/base_event.rs index 78bd927612..03a2b6bdd3 100644 --- a/crates/openshell-ocsf/src/events/base_event.rs +++ b/crates/openshell-ocsf/src/events/base_event.rs @@ -63,10 +63,6 @@ pub struct BaseEventData { #[serde(skip_serializing_if = "Option::is_none")] pub device: Option, - /// Container info (Container profile). - #[serde(skip_serializing_if = "Option::is_none")] - pub container: Option, - /// AI model info (`ai_operation` profile, OCSF v1.8.0+). #[serde(skip_serializing_if = "Option::is_none")] pub ai_model: Option, @@ -113,9 +109,6 @@ impl Serialize for BaseEventData { if let Some(ref device) = self.device { map.serialize_entry("device", device)?; } - if let Some(ref container) = self.container { - map.serialize_entry("container", container)?; - } if let Some(ref ai_model) = self.ai_model { map.serialize_entry("ai_model", ai_model)?; } @@ -160,7 +153,6 @@ impl BaseEventData { status_detail: None, metadata, device: None, - container: None, ai_model: None, unmapped: None, } @@ -191,9 +183,10 @@ impl BaseEventData { self.device = Some(device); } - /// Set container info. - pub fn set_container(&mut self, container: Container) { - self.container = Some(container); + /// The sandbox the event concerns, carried by `device.container`. + #[must_use] + pub fn container(&self) -> Option<&Container> { + self.device.as_ref()?.container.as_ref() } /// Set AI model info (`ai_operation` profile). diff --git a/crates/openshell-ocsf/src/format/downgrade.rs b/crates/openshell-ocsf/src/format/downgrade.rs index 06bd4edd24..402c7e8a91 100644 --- a/crates/openshell-ocsf/src/format/downgrade.rs +++ b/crates/openshell-ocsf/src/format/downgrade.rs @@ -42,7 +42,7 @@ impl std::str::FromStr for OcsfSchemaVersion { } /// Fields to strip when downgrading to v1.3.0 or earlier. -const STRIP_FOR_V1_3: &[&str] = &["ai_model", "container", "observation_point_id"]; +const STRIP_FOR_V1_3: &[&str] = &["ai_model", "observation_point_id"]; /// Profile names to remove from `metadata.profiles` when downgrading to v1.3.0 or earlier. const STRIP_PROFILES_V1_3: &[&str] = &["ai_operation", "container"]; @@ -75,6 +75,13 @@ pub fn downgrade_event(event: &mut Value, target_version: &str) -> bool { modified = true; } } + if obj + .get_mut("device") + .and_then(Value::as_object_mut) + .is_some_and(|device| device.remove("container").is_some()) + { + modified = true; + } if let Some(profiles) = obj .get_mut("metadata") @@ -148,8 +155,7 @@ mod tests { "version": crate::OCSF_VERSION, "profiles": ["security_control", "network_proxy", "container", "host"] }, - "device": {"hostname": "sandbox-1"}, - "container": {"name": "test-sandbox"}, + "device": {"hostname": "sandbox-1", "container": {"name": "test-sandbox"}}, "observation_point_id": 2, "unmapped": {"key": "value"} }) @@ -161,9 +167,9 @@ mod tests { let modified = downgrade_event(&mut event, "1.3.0"); assert!(modified); - assert!(event.get("container").is_none()); + assert!(event["device"].get("container").is_none()); assert!(event.get("observation_point_id").is_none()); - assert!(event.get("device").is_some()); + assert_eq!(event["device"]["hostname"], "sandbox-1"); assert!(event.get("unmapped").is_some()); } @@ -173,7 +179,7 @@ mod tests { let modified = downgrade_event(&mut event, "1.1.0"); assert!(modified); - assert!(event.get("container").is_none()); + assert!(event["device"].get("container").is_none()); assert!(event.get("observation_point_id").is_none()); } @@ -249,7 +255,7 @@ mod tests { "version": crate::OCSF_VERSION, "profiles": ["container"] }, - "container": {"name": "sandbox-1"} + "device": {"container": {"name": "sandbox-1"}} }); let modified = downgrade_event(&mut event, "1.1.0"); diff --git a/crates/openshell-ocsf/src/lib.rs b/crates/openshell-ocsf/src/lib.rs index 5b048f776a..b3f0a04f3c 100644 --- a/crates/openshell-ocsf/src/lib.rs +++ b/crates/openshell-ocsf/src/lib.rs @@ -56,8 +56,8 @@ pub use enums::{ // --- Object types --- pub use objects::{ Actor, AiModel, Api, Attack, ConnectionInfo, Container, Device, Endpoint, Evidence, - FindingInfo, FirewallRule, HttpRequest, HttpResponse, Image, Metadata, OsInfo, Process, - Product, Remediation, ResourceDetails, Service, Tactic, Technique, Url, User, + FindingInfo, FirewallRule, HttpRequest, HttpResponse, Metadata, OsInfo, Process, Product, + Remediation, ResourceDetails, Service, Tactic, Technique, Url, User, }; // --- Sanitizers --- diff --git a/crates/openshell-ocsf/src/objects/container.rs b/crates/openshell-ocsf/src/objects/container.rs index 7d55224579..efe5ed122c 100644 --- a/crates/openshell-ocsf/src/objects/container.rs +++ b/crates/openshell-ocsf/src/objects/container.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! OCSF `container` and `image` objects. +//! OCSF `container` object. use serde::{Deserialize, Serialize}; @@ -14,17 +14,6 @@ pub struct Container { /// Container unique identifier. #[serde(skip_serializing_if = "Option::is_none")] pub uid: Option, - - /// Container image. - #[serde(skip_serializing_if = "Option::is_none")] - pub image: Option, -} - -/// OCSF Image object. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Image { - /// Image name (e.g., "ghcr.io/openshell/sandbox:latest"). - pub name: String, } #[cfg(test)] @@ -36,15 +25,11 @@ mod tests { let container = Container { name: "my-sandbox".to_string(), uid: Some("sandbox-abc123".to_string()), - image: Some(Image { - name: "ghcr.io/openshell/sandbox:latest".to_string(), - }), }; let json = serde_json::to_value(&container).unwrap(); assert_eq!(json["name"], "my-sandbox"); assert_eq!(json["uid"], "sandbox-abc123"); - assert_eq!(json["image"]["name"], "ghcr.io/openshell/sandbox:latest"); } #[test] @@ -52,10 +37,8 @@ mod tests { let container = Container { name: "test".to_string(), uid: None, - image: None, }; let json = serde_json::to_value(&container).unwrap(); assert!(json.get("uid").is_none()); - assert!(json.get("image").is_none()); } } diff --git a/crates/openshell-ocsf/src/objects/device.rs b/crates/openshell-ocsf/src/objects/device.rs index 37fbf93fc6..6ca21dd013 100644 --- a/crates/openshell-ocsf/src/objects/device.rs +++ b/crates/openshell-ocsf/src/objects/device.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::enums::{DeviceTypeId, OsTypeId}; +use crate::objects::Container; /// OCSF Device object. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -31,6 +32,10 @@ pub struct Device { /// Operating system info. #[serde(skip_serializing_if = "Option::is_none")] pub os: Option, + + /// Sandbox the event concerns (`container` profile). + #[serde(skip_serializing_if = "Option::is_none")] + pub container: Option, } /// OCSF OS Info object. @@ -78,30 +83,36 @@ impl OsInfo { } impl Device { - /// Create a Linux sandbox device with the given hostname. - #[must_use] - pub fn linux(hostname: &str) -> Self { + /// Create a sandbox device with the given hostname for a `std::env::consts::OS` value. + fn sandbox(hostname: &str, os: &str) -> Self { Self { hostname: hostname.to_string(), name: None, uid: None, type_id: DeviceTypeId::Other, type_label: "Sandbox".to_string(), - os: Some(OsInfo::for_target("linux")), + os: Some(OsInfo::for_target(os)), + container: None, } } + /// Create a Linux sandbox device with the given hostname. + #[must_use] + pub fn linux(hostname: &str) -> Self { + Self::sandbox(hostname, "linux") + } + /// Create a Windows device with the given hostname. #[must_use] pub fn windows(hostname: &str) -> Self { - Self { - hostname: hostname.to_string(), - name: None, - uid: None, - type_id: DeviceTypeId::Other, - type_label: "Sandbox".to_string(), - os: Some(OsInfo::for_target("windows")), - } + Self::sandbox(hostname, "windows") + } + + /// Attach the sandbox the event concerns. + #[must_use] + pub fn with_container(mut self, container: Option) -> Self { + self.container = container; + self } /// Create a device stamped with the OS this build is running on. @@ -132,6 +143,7 @@ impl Device { // Operators assign a unique name per installation; replicas share this UID. uid: Some(name.to_string()), os: Some(OsInfo::for_target(std::env::consts::OS)), + container: None, } } } diff --git a/crates/openshell-ocsf/src/objects/mod.rs b/crates/openshell-ocsf/src/objects/mod.rs index 9025834bfe..9a6c8fc539 100644 --- a/crates/openshell-ocsf/src/objects/mod.rs +++ b/crates/openshell-ocsf/src/objects/mod.rs @@ -23,7 +23,7 @@ pub use ai_model::AiModel; pub use api::Api; pub use attack::{Attack, Tactic, Technique}; pub use connection::ConnectionInfo; -pub use container::{Container, Image}; +pub use container::Container; pub use device::{Device, OsInfo}; pub use endpoint::Endpoint; pub use finding::{Evidence, FindingInfo, Remediation}; diff --git a/crates/openshell-ocsf/src/validation/schema.rs b/crates/openshell-ocsf/src/validation/schema.rs index 88f80256b4..b13399153e 100644 --- a/crates/openshell-ocsf/src/validation/schema.rs +++ b/crates/openshell-ocsf/src/validation/schema.rs @@ -106,27 +106,14 @@ fn validate(value: &Value, schema: &Value, check_required: bool) { } else { schema["name"].as_str().unwrap_or("object") }; - validate_attributes( - value, - schema, - declared.as_deref(), - root, - is_event, - check_required, - ); + validate_attributes(value, schema, declared.as_deref(), root, check_required); } -/// Top-level attributes `OpenShell` emits that no OCSF version defines, pending a -/// design decision. Remove an entry once the emitters stop producing it. -/// - `container`: identifies the affected sandbox (NVIDIA/OpenShell#4915). -const KNOWN_UNDEFINED_EVENT_ATTRIBUTES: &[&str] = &["container"]; - fn validate_attributes( value: &Value, schema: &Value, declared: Option<&[&str]>, path: &str, - event_root: bool, check_required: bool, ) { let attrs = attribute_map(schema); @@ -150,9 +137,6 @@ fn validate_attributes( } for name in obj.keys() { - if event_root && KNOWN_UNDEFINED_EVENT_ATTRIBUTES.contains(&name.as_str()) { - continue; - } let Some(def) = attrs.get(name) else { panic!("Undefined attribute '{path}.{name}'"); }; @@ -192,24 +176,12 @@ fn validate_attributes( match child { Value::Array(items) => { for item in items { - validate_attributes( - item, - &child_schema, - declared, - &child_path, - false, - check_required, - ); + validate_attributes(item, &child_schema, declared, &child_path, check_required); } } - other => validate_attributes( - other, - &child_schema, - declared, - &child_path, - false, - check_required, - ), + other => { + validate_attributes(other, &child_schema, declared, &child_path, check_required); + } } } } @@ -319,6 +291,17 @@ mod tests { assert!(message.contains("container"), "{message}"); } + #[test] + fn rejects_a_top_level_container() { + let schema = load_class_schema("base_event"); + let mut event = minimal_base_event(); + event["container"] = serde_json::json!({"name": "sandbox"}); + let message = panic_message(std::panic::catch_unwind(|| { + validate_known_fields(&event, &schema); + })); + assert!(message.contains("event.container"), "{message}"); + } + #[test] fn object_schemas_are_validated_as_objects_not_events() { let device = load_object_schema("device"); @@ -330,8 +313,7 @@ mod tests { }); validate_required_fields(&value, &device); - // The top-level `container` exemption applies to events only, so an - // object whose schema does not define `container` rejects it. + // An object whose schema does not define `container` rejects it. let user = load_object_schema("user"); let value = serde_json::json!({"name": "u", "container": {"name": "sandbox"}}); let message = panic_message(std::panic::catch_unwind(|| { diff --git a/crates/openshell-ocsf/tests/event_identity.rs b/crates/openshell-ocsf/tests/event_identity.rs index f876d3c2ee..4471ec9be8 100644 --- a/crates/openshell-ocsf/tests/event_identity.rs +++ b/crates/openshell-ocsf/tests/event_identity.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! `metadata.uid` identifies the event; `container.uid` identifies the sandbox. +//! `metadata.uid` identifies the event; `device.container.uid` identifies the sandbox. use std::net::{IpAddr, Ipv4Addr}; @@ -56,14 +56,17 @@ fn the_sandbox_id_is_carried_by_the_container() { .to_json() .expect("serializes"); - assert_eq!(json["container"]["uid"], "sb-1"); - assert_eq!(json["container"]["name"], "agent-01"); + assert_eq!(json["device"]["container"]["uid"], "sb-1"); + assert_eq!(json["device"]["container"]["name"], "agent-01"); + assert!(json.get("container").is_none()); } #[test] -fn a_container_without_an_image_omits_the_image() { - let json = event(&sandbox_ctx("")).to_json().expect("serializes"); - assert!(json["container"].get("image").is_none()); +fn the_container_omits_the_image_which_ocsf_requires_an_id_for() { + let json = event(&sandbox_ctx("ghcr.io/nvidia/openshell/sandbox:0.42.1")) + .to_json() + .expect("serializes"); + assert!(json["device"]["container"].get("image").is_none()); } #[test] diff --git a/crates/openshell-ocsf/tests/gateway_context.rs b/crates/openshell-ocsf/tests/gateway_context.rs index f96f4f3cef..d60000d417 100644 --- a/crates/openshell-ocsf/tests/gateway_context.rs +++ b/crates/openshell-ocsf/tests/gateway_context.rs @@ -127,7 +127,7 @@ fn gateway_events_omit_the_container_object() { let json = event.to_json().unwrap(); assert!( - json.get("container").is_none(), + json["device"].get("container").is_none(), "a gateway event without a sandbox association should omit container: {json}" ); } @@ -142,7 +142,7 @@ fn supervisor_events_still_carry_their_sandbox_container() { .build(); let json = event.to_json().unwrap(); - assert_eq!(json["container"]["name"], "agent-01"); - assert_eq!(json["container"]["uid"], "sb-1"); + assert_eq!(json["device"]["container"]["name"], "agent-01"); + assert_eq!(json["device"]["container"]["uid"], "sb-1"); assert!(json["device"].get("name").is_none()); } diff --git a/crates/openshell-otel/src/ocsf.rs b/crates/openshell-otel/src/ocsf.rs index 2dc33cee8f..0cf4d01657 100644 --- a/crates/openshell-otel/src/ocsf.rs +++ b/crates/openshell-otel/src/ocsf.rs @@ -402,8 +402,7 @@ fn log_record( record.add_attribute("ocsf.metadata.uid", uid.clone()); } if let Some(sandbox_id) = base - .container - .as_ref() + .container() .and_then(|container| container.uid.clone()) .filter(|id| !id.is_empty()) { @@ -506,7 +505,7 @@ mod tests { let event = event("sandbox-1", SeverityId::Informational); let body = body(&record(&event, Some(OcsfSchemaVersion::V1_3))); assert_eq!(body["metadata"]["version"], "1.3"); - assert!(body.get("container").is_none()); + assert!(body["device"].get("container").is_none()); } #[test] diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index b09a6df792..874c24a61b 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -21402,8 +21402,7 @@ mod tests { assert_eq!(events.len(), 2); for event in &events { assert_eq!(event["metadata"]["product"]["name"], "OpenShell Gateway"); - assert_eq!(event["container"]["uid"], "sb-123"); - assert!(event["container"].get("image").is_none()); + assert_eq!(event["device"]["container"]["uid"], "sb-123"); } assert_eq!(events[0]["unmapped"]["policy_hash"], "sha256:manual"); assert!(events[0]["unmapped"].get("auto").is_none()); @@ -21514,7 +21513,7 @@ mod tests { let rejected = rejected_events(&events); assert_eq!(rejected.len(), 1, "events: {events:?}"); let event = rejected[0]; - assert_eq!(event["container"]["uid"], "sb-reject-audit"); + assert_eq!(event["device"]["container"]["uid"], "sb-reject-audit"); assert_eq!(event["unmapped"]["chunk_id"], chunk_id); assert_eq!( event["unmapped"]["rejection_reason"], diff --git a/crates/openshell-server/src/ocsf_log.rs b/crates/openshell-server/src/ocsf_log.rs index 8858c9c587..d40abde9f7 100644 --- a/crates/openshell-server/src/ocsf_log.rs +++ b/crates/openshell-server/src/ocsf_log.rs @@ -531,7 +531,7 @@ mod tests { "TLS certificate config reloaded successfully" ); assert_eq!(event["metadata"]["product"]["name"], "OpenShell Gateway"); - assert!(event.get("container").is_none()); + assert!(event["device"].get("container").is_none()); } #[test] diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index 522a004965..5d61fe4aee 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -353,8 +353,7 @@ where // affected sandbox, not by the gateway that produced the event. visitor.sandbox_id = ocsf .base() - .container - .as_ref() + .container() .and_then(|container| container.uid.clone()) .filter(|id| !id.is_empty()); visitor.message = Some(ocsf.format_shorthand()); diff --git a/docs/observability/ocsf-json-export.mdx b/docs/observability/ocsf-json-export.mdx index f7ec64ef33..e157cbed1c 100644 --- a/docs/observability/ocsf-json-export.mdx +++ b/docs/observability/ocsf-json-export.mdx @@ -145,7 +145,7 @@ Gateway-produced records use the `OpenShell Gateway` product identity; superviso For gateway-produced records, `device.os.name` and `device.os.type_id` identify the OS running the gateway process: Linux (200), Windows (100), or macOS (300). Linux supervisor records continue to report Linux, regardless of the gateway's OS. A native macOS gateway therefore reports macOS while its Linux sandboxes report Linux; a gateway running inside a Linux container reports Linux, even on a Mac host. -`metadata.uid` uniquely identifies an event and stays unchanged when that record is serialized again. Use `container.uid` to associate the event with its sandbox, not `metadata.uid`. Records without a sandbox association omit the container; unknown images are omitted rather than represented by an empty image name. +`metadata.uid` uniquely identifies an event and stays unchanged when that record is serialized again. Use `device.container.uid` to associate the event with its sandbox, not `metadata.uid`. The sandbox name is `device.container.name`. Records without a sandbox association omit the container. The container carries no image, because OCSF requires an image ID that OpenShell does not know. Some fields depend on the event class: @@ -153,7 +153,7 @@ Some fields depend on the event class: - Process Activity records from the sandbox name the `openshell-sandbox` launcher as `actor.process`. - `connection_info` carries `direction_id` and `direction`. OpenShell reports Unknown (`0`) where it does not track the direction. -Each JSONL file line, or the payload after the stderr marker, is a complete OCSF v1.8.0 JSON object when no downgrade is configured. Records validate against OCSF v1.8.0 except for the top-level `container` object, which OCSF does not define at the event level. Here is an example of a network connection event: +Each JSONL file line, or the payload after the stderr marker, is a complete OCSF v1.8.0 JSON object when no downgrade is configured. Records validate against OCSF v1.8.0. Here is an example of a network connection event: ```json { @@ -244,12 +244,12 @@ And a denied connection, produced when `curl https://github.com` runs in a sandb "name": "Linux", "type_id": 200, "type": "Linux" + }, + "container": { + "name": "docs-deny", + "uid": "3949f6fc-2b84-45b2-a631-46debe41d84f" } }, - "container": { - "name": "docs-deny", - "uid": "3949f6fc-2b84-45b2-a631-46debe41d84f" - }, "dst_endpoint": { "domain": "github.com", "ip": "198.18.0.2", @@ -338,7 +338,7 @@ The setting takes effect on the next poll cycle, by default every 10 seconds. No Supported target versions are `1.1` and `1.3`. When set, the JSONL layer applies the following transformations: -- Strips fields added after the target version: `ai_model`, `container`, `observation_point_id` +- Strips fields added after the target version: `ai_model`, `device.container`, `observation_point_id` - Removes unknown profile names from `metadata.profiles`: `ai_operation`, `container` - Rewrites `metadata.version` to match the target version diff --git a/e2e/rust/tests/ocsf_console.rs b/e2e/rust/tests/ocsf_console.rs index 9ce14a753c..4a62be40c9 100644 --- a/e2e/rust/tests/ocsf_console.rs +++ b/e2e/rust/tests/ocsf_console.rs @@ -126,7 +126,7 @@ async fn network_events_are_json_on_supervisor_stderr_only() { .as_str() .is_some_and(|uid| !uid.is_empty()) ); - assert_eq!(record["container"]["name"], sandbox.name); + assert_eq!(record["device"]["container"]["name"], sandbox.name); } // Allow the existing best-effort push batch to reach the gateway. sleep(Duration::from_secs(2)).await;