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": {