From e69fce13bb7e0e2c808fb0cd66b2463d0dc7f564 Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Fri, 16 Oct 2026 16:12:52 -0700 Subject: [PATCH] fix(ocsf): report the required device.os.type_id OCSF 1.8 requires type_id on the os object, but device.os carried only a name. Events now report Linux (200), Windows (100), macOS (300), or Other (99) with the platform name as the type label. The os object schema is vendored so tests validate it. Refs #2912 Signed-off-by: Kris Hicks --- crates/openshell-ocsf/schemas/ocsf/README.md | 6 +- .../schemas/ocsf/v1.8.0/objects/os.json | 153 ++++++++++++++++++ crates/openshell-ocsf/src/enums/mod.rs | 2 + crates/openshell-ocsf/src/enums/os_type.rs | 23 +++ crates/openshell-ocsf/src/lib.rs | 4 +- crates/openshell-ocsf/src/objects/device.rs | 74 +++++++-- docs/observability/ocsf-json-export.mdx | 6 +- 7 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.8.0/objects/os.json create mode 100644 crates/openshell-ocsf/src/enums/os_type.rs diff --git a/crates/openshell-ocsf/schemas/ocsf/README.md b/crates/openshell-ocsf/schemas/ocsf/README.md index 401ebc3392..3f1a70ddf5 100644 --- a/crates/openshell-ocsf/schemas/ocsf/README.md +++ b/crates/openshell-ocsf/schemas/ocsf/README.md @@ -22,12 +22,12 @@ for offline test validation. - `authentication` [3002] - `base_event` [0] -### Objects (21) +### Objects (22) - `metadata`, `network_endpoint`, `network_proxy`, `process`, `actor` - `device`, `container`, `product`, `firewall_rule`, `finding_info` - `evidences`, `http_request`, `http_response`, `url`, `attack` -- `remediation`, `connection_info`, `ai_model`, `api`, `user`, `service` +- `remediation`, `connection_info`, `ai_model`, `api`, `user`, `service`, `os` ### Profiles (1) @@ -50,7 +50,7 @@ done for object in metadata network_endpoint network_proxy process actor device \ container product firewall_rule finding_info evidences \ http_request http_response url attack remediation connection_info \ - ai_model api user service; do + ai_model api user service os; do curl -s "https://schema.ocsf.io/api/${VERSION}/objects/${object}" \ | python3 -m json.tool > "objects/${object}.json" done diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.8.0/objects/os.json b/crates/openshell-ocsf/schemas/ocsf/v1.8.0/objects/os.json new file mode 100644 index 0000000000..512ec2294f --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.8.0/objects/os.json @@ -0,0 +1,153 @@ +{ + "attributes": { + "name": { + "type": "string_t", + "description": "The operating system name.", + "requirement": "required", + "caption": "Name", + "type_name": "String" + }, + "type": { + "type": "string_t", + "description": "The type of the operating system.", + "requirement": "optional", + "caption": "Type", + "type_name": "String" + }, + "version": { + "type": "string_t", + "description": "The version of the OS running on the device that originated the event. For example: \"Windows 10\", \"OS X 10.7\", or \"iOS 9\".", + "requirement": "optional", + "caption": "Version", + "type_name": "String" + }, + "build": { + "type": "string_t", + "description": "The operating system build number.", + "requirement": "optional", + "caption": "OS Build", + "type_name": "String" + }, + "country": { + "type": "string_t", + "description": "The operating system country code, as defined by the ISO 3166-1 standard (Alpha-2 code).

Note: The two letter country code should be capitalized. For example: US or CA.

", + "references": [ + { + "description": "ISO 3166-1 alpha-2 codes", + "url": "https://www.iso.org/obp/ui/#iso:pub:PUB500001:en" + } + ], + "requirement": "optional", + "caption": "Country", + "type_name": "String", + "observable": 14 + }, + "lang": { + "type": "string_t", + "description": "The two letter lower case language codes, as defined by ISO 639-1. For example: en (English), de (German), or fr (French).", + "requirement": "optional", + "caption": "Language", + "type_name": "String" + }, + "type_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The type is unknown.", + "caption": "Unknown" + }, + "99": { + "description": "The type is not mapped. See the type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "101": { + "caption": "Windows Mobile" + }, + "100": { + "caption": "Windows" + }, + "200": { + "caption": "Linux" + }, + "201": { + "caption": "Android" + }, + "300": { + "caption": "macOS" + }, + "301": { + "caption": "iOS" + }, + "302": { + "caption": "iPadOS" + }, + "400": { + "caption": "Solaris" + }, + "401": { + "caption": "AIX" + }, + "402": { + "caption": "HP-UX" + } + }, + "description": "The type identifier of the operating system.", + "requirement": "required", + "sibling": "type", + "caption": "Type ID", + "type_name": "Integer" + }, + "cpe_name": { + "type": "string_t", + "description": "The Common Platform Enumeration (CPE) name as described by (NIST) For example: cpe:/a:apple:safari:16.2.", + "requirement": "optional", + "caption": "The product CPE identifier", + "type_name": "String" + }, + "cpu_bits": { + "type": "integer_t", + "description": "The cpu architecture, the number of bits used for addressing in memory. For example: 32 or 64.", + "requirement": "optional", + "caption": "CPU Bits", + "type_name": "Integer" + }, + "edition": { + "type": "string_t", + "description": "The operating system edition. For example: Professional.", + "requirement": "optional", + "caption": "OS Edition", + "type_name": "String" + }, + "kernel_release": { + "type": "string_t", + "description": "The kernel release of the operating system. On Unix-based systems, this is determined from the uname -r command output, for example \"5.15.0-122-generic\".", + "requirement": "optional", + "caption": "Kernel Release", + "type_name": "String" + }, + "sp_name": { + "type": "string_t", + "description": "The name of the latest Service Pack.", + "requirement": "optional", + "caption": "OS Service Pack", + "type_name": "String" + }, + "sp_ver": { + "type": "integer_t", + "description": "The version number of the latest Service Pack.", + "requirement": "optional", + "caption": "OS Service Pack Version", + "type_name": "Integer" + } + }, + "name": "os", + "description": "The Operating System (OS) object describes characteristics of an OS, such as Linux or Windows.", + "extends": "object", + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:OperatingSystem", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:OperatingSystem/" + } + ], + "caption": "Operating System (OS)" +} diff --git a/crates/openshell-ocsf/src/enums/mod.rs b/crates/openshell-ocsf/src/enums/mod.rs index 3ad8978130..84ef4edd0f 100644 --- a/crates/openshell-ocsf/src/enums/mod.rs +++ b/crates/openshell-ocsf/src/enums/mod.rs @@ -11,6 +11,7 @@ mod device_type; mod disposition; mod http_method; mod launch; +mod os_type; mod security; mod severity; mod status; @@ -24,6 +25,7 @@ pub use device_type::DeviceTypeId; pub use disposition::DispositionId; pub use http_method::HttpMethod; pub use launch::LaunchTypeId; +pub use os_type::OsTypeId; pub use security::{ConfidenceId, RiskLevelId, SecurityLevelId}; pub use severity::SeverityId; pub use status::{StateId, StatusId}; diff --git a/crates/openshell-ocsf/src/enums/os_type.rs b/crates/openshell-ocsf/src/enums/os_type.rs new file mode 100644 index 0000000000..dbf5c3f0c6 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/os_type.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `os.type_id` enum. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF OS Type ID. +/// +/// Only the values `OpenShell` can produce are modelled; the schema defines a +/// wider set (Android, iOS, Solaris, ...). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u16)] +pub enum OsTypeId { + /// 99 — Other + Other = 99, + /// 100 — Windows + Windows = 100, + /// 200 — Linux + Linux = 200, + /// 300 — macOS + MacOs = 300, +} diff --git a/crates/openshell-ocsf/src/lib.rs b/crates/openshell-ocsf/src/lib.rs index 746aea9aac..e8eeedfeee 100644 --- a/crates/openshell-ocsf/src/lib.rs +++ b/crates/openshell-ocsf/src/lib.rs @@ -49,8 +49,8 @@ pub use events::{ // --- Enum types --- pub use enums::{ ActionId, ActivityId, AuthProtocolId, AuthTypeId, ConfidenceId, DeviceTypeId, DispositionId, - HttpMethod, LaunchTypeId, OcsfEnum, RiskLevelId, SecurityLevelId, SeverityId, StateId, - StatusId, UserTypeId, + HttpMethod, LaunchTypeId, OcsfEnum, OsTypeId, RiskLevelId, SecurityLevelId, SeverityId, + StateId, StatusId, UserTypeId, }; // --- Object types --- diff --git a/crates/openshell-ocsf/src/objects/device.rs b/crates/openshell-ocsf/src/objects/device.rs index 0984c1b7cf..37fbf93fc6 100644 --- a/crates/openshell-ocsf/src/objects/device.rs +++ b/crates/openshell-ocsf/src/objects/device.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; -use crate::enums::DeviceTypeId; +use crate::enums::{DeviceTypeId, OsTypeId}; /// OCSF Device object. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -38,9 +38,33 @@ pub struct Device { pub struct OsInfo { /// OS name (e.g., "Linux"). pub name: String, + + /// OS type id. Required by the OCSF schema. + pub type_id: OsTypeId, + + /// Sibling label for `type_id`; the platform name when `type_id` is Other. + #[serde(rename = "type")] + pub type_label: String, } impl OsInfo { + /// OS info for a `std::env::consts::OS` value. + #[must_use] + pub fn for_target(os: &str) -> Self { + let type_id = match os { + "linux" => OsTypeId::Linux, + "windows" => OsTypeId::Windows, + "macos" => OsTypeId::MacOs, + _ => OsTypeId::Other, + }; + let name = Self::pretty_name(os).to_string(); + Self { + type_label: name.clone(), + name, + type_id, + } + } + /// Display name for a `std::env::consts::OS` value. #[must_use] pub fn pretty_name(os: &str) -> &str { @@ -63,9 +87,7 @@ impl Device { uid: None, type_id: DeviceTypeId::Other, type_label: "Sandbox".to_string(), - os: Some(OsInfo { - name: "Linux".to_string(), - }), + os: Some(OsInfo::for_target("linux")), } } @@ -78,9 +100,7 @@ impl Device { uid: None, type_id: DeviceTypeId::Other, type_label: "Sandbox".to_string(), - os: Some(OsInfo { - name: "Windows".to_string(), - }), + os: Some(OsInfo::for_target("windows")), } } @@ -111,9 +131,7 @@ impl Device { name: Some(name.to_string()), // Operators assign a unique name per installation; replicas share this UID. uid: Some(name.to_string()), - os: Some(OsInfo { - name: OsInfo::pretty_name(std::env::consts::OS).to_string(), - }), + os: Some(OsInfo::for_target(std::env::consts::OS)), } } } @@ -182,6 +200,42 @@ mod tests { } } + #[test] + fn os_type_follows_the_target_os() { + for (target, name, type_id, type_label) in [ + ("linux", "Linux", 200, "Linux"), + ("windows", "Windows", 100, "Windows"), + ("macos", "macOS", 300, "macOS"), + ("freebsd", "freebsd", 99, "freebsd"), + ] { + let json = serde_json::to_value(OsInfo::for_target(target)).unwrap(); + assert_eq!(json["name"], name); + assert_eq!(json["type_id"], type_id); + assert_eq!(json["type"], type_label); + } + } + + #[test] + fn device_os_matches_vendored_schema() { + use crate::validation::schema::{ + load_object_schema, validate_enum_value, validate_known_fields, + validate_required_fields, + }; + + let schema = load_object_schema("os"); + for device in [ + Device::linux("sandbox"), + Device::windows("sandbox"), + Device::for_current_os("sandbox"), + Device::gateway("gateway-0", "production"), + ] { + let os = serde_json::to_value(device.os).unwrap(); + validate_required_fields(&os, &schema); + validate_known_fields(&os, &schema); + validate_enum_value(&os, "type_id", &schema); + } + } + #[test] fn gateway_device_does_not_inherit_the_sandbox_type() { let json = serde_json::to_value(Device::gateway("gateway-0", "production")).unwrap(); diff --git a/docs/observability/ocsf-json-export.mdx b/docs/observability/ocsf-json-export.mdx index 5d91ec187d..e6b89d69ca 100644 --- a/docs/observability/ocsf-json-export.mdx +++ b/docs/observability/ocsf-json-export.mdx @@ -122,7 +122,7 @@ Windows MXC gateway records use `%PROGRAMDATA%\OpenShell\logs` or `OPENSHELL_OCS Gateway-produced records use the `OpenShell Gateway` product identity; supervisor records use `OpenShell Sandbox Supervisor`. The producing device and the affected sandbox are separate identities, so a gateway event can still identify an affected sandbox through its container fields. -For gateway-produced records, `device.os.name` identifies the OS running the gateway process: Linux, Windows, or macOS. 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. +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. @@ -214,7 +214,9 @@ And a denied connection, produced when `curl https://github.com` runs in a sandb "type_id": 99, "type": "Sandbox", "os": { - "name": "Linux" + "name": "Linux", + "type_id": 200, + "type": "Linux" } }, "container": {