Files
OpenShell/crates/openshell-ocsf/tests/gateway_context.rs
krishicksandEvan Lezar a875add234 feat(server): write gateway OCSF events to JSONL (#3264)
* feat(server): write gateway OCSF events to JSONL

Previously, gateway security activity was available only in diagnostic output,
and events not associated with a sandbox, such as TLS certificate reloads, had
no independent structured record.

Now, configuring `openshell.gateway.ocsf_log` writes every gateway-produced
OCSF record to a bounded JSONL destination independently of `RUST_LOG`. The
destination supports daily or disabled rotation, retention limits, queue bounds,
and optional schema downgrade to OCSF 1.1 or 1.3.

Additionally, existing gateway emitters (TLS reloads, service routing, and
policy approval and auto-approval audits) emit structured events, so they reach
the JSONL destination, console shorthand, and the affected sandbox's log
stream. Records identify the gateway by its configured name in `device.uid` and
`device.name`, shared across replicas, with `device.hostname` identifying the
replica and `device.os` the gateway's operating system. Metrics and warnings
expose known best-effort losses.

Refs #2762

Signed-off-by: Kris Hicks <khicks@nvidia.com>

* fix(mxc): attribute ETW events to gateway

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Kris Hicks <khicks@nvidia.com>
Signed-off-by: Evan Lezar <elezar@nvidia.com>
Co-authored-by: Evan Lezar <elezar@nvidia.com>
2026-09-29 16:19:53 +00:00

149 lines
4.6 KiB
Rust

// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Gateway-origin events carry gateway identity, not sandbox identity.
use std::net::{IpAddr, Ipv4Addr};
use openshell_ocsf::{
ActivityId, AppLifecycleBuilder, ConfigStateChangeBuilder, EventContext, EventOrigin, OsInfo,
SeverityId, StateId, StatusId,
};
fn gateway_ctx() -> EventContext {
EventContext {
sandbox_id: String::new(),
sandbox_name: String::new(),
container_image: String::new(),
hostname: "openshell-gateway-0".to_string(),
product_version: "0.42.1".to_string(),
proxy_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
proxy_port: 0,
origin: EventOrigin::Gateway {
name: "production-us-west".to_string(),
},
}
}
fn supervisor_ctx() -> EventContext {
EventContext {
sandbox_id: "sb-1".to_string(),
sandbox_name: "agent-01".to_string(),
container_image: "ghcr.io/nvidia/openshell/sandbox:0.42.1".to_string(),
hostname: "openshell-sb-1".to_string(),
product_version: "0.42.1".to_string(),
proxy_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
proxy_port: 8888,
origin: EventOrigin::Supervisor,
}
}
#[test]
fn gateway_events_report_the_gateway_os() {
let expected_os = OsInfo::pretty_name(std::env::consts::OS);
for sandbox_id in ["", "sb-1"] {
let mut context = gateway_ctx();
context.sandbox_id = sandbox_id.to_string();
let json = AppLifecycleBuilder::new(&context)
.activity(ActivityId::Open)
.build()
.to_json()
.unwrap();
assert_eq!(json["device"]["os"]["name"], expected_os);
}
}
#[test]
fn supervisor_events_preserve_platform_os() {
let json = AppLifecycleBuilder::new(&supervisor_ctx())
.activity(ActivityId::Open)
.build()
.to_json()
.unwrap();
let expected_os = if cfg!(target_os = "windows") {
"Windows"
} else {
"Linux"
};
assert_eq!(json["device"]["os"]["name"], expected_os);
}
#[test]
fn gateway_events_report_the_gateway_product() {
let event = AppLifecycleBuilder::new(&gateway_ctx())
.activity(ActivityId::Open)
.severity(SeverityId::Informational)
.message("gateway started")
.build();
let json = event.to_json().unwrap();
assert_eq!(json["metadata"]["product"]["name"], "OpenShell Gateway");
assert_eq!(json["metadata"]["product"]["vendor_name"], "OpenShell");
assert_eq!(json["metadata"]["version"], "1.8.0");
assert_eq!(json["metadata"]["product"]["version"], "0.42.1");
}
#[test]
fn supervisor_events_still_report_the_supervisor_product() {
let event = AppLifecycleBuilder::new(&supervisor_ctx())
.activity(ActivityId::Open)
.severity(SeverityId::Informational)
.message("supervisor started")
.build();
let json = event.to_json().unwrap();
assert_eq!(
json["metadata"]["product"]["name"],
"OpenShell Sandbox Supervisor"
);
}
#[test]
fn gateway_events_identify_the_device_by_operator_assigned_name() {
let event = ConfigStateChangeBuilder::new(&gateway_ctx())
.state(StateId::Enabled, "reloaded")
.severity(SeverityId::Informational)
.status(StatusId::Success)
.message("TLS certificate config reloaded")
.build();
let json = event.to_json().unwrap();
assert_eq!(json["device"]["name"], "production-us-west");
assert_eq!(json["device"]["uid"], "production-us-west");
assert_eq!(json["device"]["hostname"], "openshell-gateway-0");
}
#[test]
fn gateway_events_omit_the_container_object() {
let event = ConfigStateChangeBuilder::new(&gateway_ctx())
.state(StateId::Enabled, "reloaded")
.severity(SeverityId::Informational)
.status(StatusId::Success)
.message("TLS certificate config reloaded")
.build();
let json = event.to_json().unwrap();
assert!(
json.get("container").is_none(),
"a gateway event without a sandbox association should omit container: {json}"
);
}
#[test]
fn supervisor_events_still_carry_their_sandbox_container() {
let event = ConfigStateChangeBuilder::new(&supervisor_ctx())
.state(StateId::Enabled, "loaded")
.severity(SeverityId::Informational)
.status(StatusId::Success)
.message("policy loaded")
.build();
let json = event.to_json().unwrap();
assert_eq!(json["container"]["name"], "agent-01");
assert_eq!(json["container"]["uid"], "sb-1");
assert!(json["device"].get("name").is_none());
}