From f443e3387ee41e84095193030590952c4ab9d46d Mon Sep 17 00:00:00 2001 From: John Myers Date: Thu, 19 Mar 2026 16:40:04 -0700 Subject: [PATCH 01/11] feat(ocsf): create openshell-ocsf crate with OCSF v1.7.0 event types, formatters, and tracing layers Closes #392 Standalone crate implementing the OCSF v1.7.0 event model for sandbox logging. Includes 8 event classes (Network Activity, HTTP Activity, SSH Activity, Process Activity, Detection Finding, Application Lifecycle, Device Config State Change, Base Event), 11 typed enum types, 20 object types, per-class builders with SandboxContext, dual formatters (shorthand single-line + JSONL), tracing layers (OcsfShorthandLayer, OcsfJsonlLayer), ocsf_emit! macro, and vendored OCSF v1.7.0 schemas with test validation utilities. 93 tests covering all event classes, formatters, builders, and schema validation. --- Cargo.lock | 12 + crates/openshell-ocsf/Cargo.toml | 25 + crates/openshell-ocsf/schemas/ocsf/README.md | 53 + .../schemas/ocsf/v1.7.0/VERSION | 1 + .../v1.7.0/classes/application_lifecycle.json | 1047 ++++++++++++ .../ocsf/v1.7.0/classes/base_event.json | 964 ++++++++++++ .../v1.7.0/classes/detection_finding.json | 1397 +++++++++++++++++ .../classes/device_config_state_change.json | 1137 ++++++++++++++ .../ocsf/v1.7.0/classes/http_activity.json | 1380 ++++++++++++++++ .../ocsf/v1.7.0/classes/network_activity.json | 1309 +++++++++++++++ .../ocsf/v1.7.0/classes/process_activity.json | 1193 ++++++++++++++ .../ocsf/v1.7.0/classes/ssh_activity.json | 1391 ++++++++++++++++ .../schemas/ocsf/v1.7.0/objects/actor.json | 118 ++ .../schemas/ocsf/v1.7.0/objects/attack.json | 135 ++ .../ocsf/v1.7.0/objects/connection_info.json | 3 + .../ocsf/v1.7.0/objects/container.json | 150 ++ .../schemas/ocsf/v1.7.0/objects/device.json | 798 ++++++++++ .../ocsf/v1.7.0/objects/evidences.json | 428 +++++ .../ocsf/v1.7.0/objects/finding_info.json | 318 ++++ .../ocsf/v1.7.0/objects/firewall_rule.json | 135 ++ .../ocsf/v1.7.0/objects/http_request.json | 167 ++ .../ocsf/v1.7.0/objects/http_response.json | 96 ++ .../schemas/ocsf/v1.7.0/objects/metadata.json | 431 +++++ .../ocsf/v1.7.0/objects/network_endpoint.json | 448 ++++++ .../ocsf/v1.7.0/objects/network_proxy.json | 448 ++++++ .../schemas/ocsf/v1.7.0/objects/process.json | 446 ++++++ .../schemas/ocsf/v1.7.0/objects/product.json | 139 ++ .../ocsf/v1.7.0/objects/remediation.json | 74 + .../schemas/ocsf/v1.7.0/objects/url.json | 404 +++++ crates/openshell-ocsf/src/builders/base.rs | 114 ++ crates/openshell-ocsf/src/builders/config.rs | 144 ++ crates/openshell-ocsf/src/builders/finding.rs | 221 +++ crates/openshell-ocsf/src/builders/http.rs | 180 +++ .../openshell-ocsf/src/builders/lifecycle.rs | 104 ++ crates/openshell-ocsf/src/builders/mod.rs | 141 ++ crates/openshell-ocsf/src/builders/network.rs | 235 +++ crates/openshell-ocsf/src/builders/process.rs | 173 ++ crates/openshell-ocsf/src/builders/ssh.rs | 174 ++ crates/openshell-ocsf/src/enums/action.rs | 67 + crates/openshell-ocsf/src/enums/activity.rs | 180 +++ crates/openshell-ocsf/src/enums/auth.rs | 79 + .../openshell-ocsf/src/enums/disposition.rs | 148 ++ crates/openshell-ocsf/src/enums/launch.rs | 63 + crates/openshell-ocsf/src/enums/mod.rs | 22 + crates/openshell-ocsf/src/enums/security.rs | 160 ++ crates/openshell-ocsf/src/enums/severity.rs | 115 ++ crates/openshell-ocsf/src/enums/status.rs | 107 ++ .../src/events/app_lifecycle.rs | 61 + .../openshell-ocsf/src/events/base_event.rs | 248 +++ .../src/events/config_state_change.rs | 89 ++ .../src/events/detection_finding.rs | 128 ++ .../src/events/http_activity.rs | 121 ++ crates/openshell-ocsf/src/events/mod.rs | 78 + .../src/events/network_activity.rs | 119 ++ .../src/events/process_activity.rs | 98 ++ .../openshell-ocsf/src/events/ssh_activity.rs | 102 ++ crates/openshell-ocsf/src/format/jsonl.rs | 97 ++ crates/openshell-ocsf/src/format/mod.rs | 7 + crates/openshell-ocsf/src/format/shorthand.rs | 545 +++++++ crates/openshell-ocsf/src/lib.rs | 65 + crates/openshell-ocsf/src/objects/attack.rs | 82 + .../openshell-ocsf/src/objects/connection.rs | 35 + .../openshell-ocsf/src/objects/container.rs | 61 + crates/openshell-ocsf/src/objects/device.rs | 50 + crates/openshell-ocsf/src/objects/endpoint.rs | 89 ++ crates/openshell-ocsf/src/objects/finding.rs | 111 ++ .../src/objects/firewall_rule.rs | 41 + crates/openshell-ocsf/src/objects/http.rs | 106 ++ crates/openshell-ocsf/src/objects/metadata.rs | 92 ++ crates/openshell-ocsf/src/objects/mod.rs | 26 + crates/openshell-ocsf/src/objects/process.rs | 140 ++ .../src/tracing_layers/event_bridge.rs | 104 ++ .../src/tracing_layers/jsonl_layer.rs | 60 + .../openshell-ocsf/src/tracing_layers/mod.rs | 16 + .../src/tracing_layers/shorthand_layer.rs | 106 ++ crates/openshell-ocsf/src/validation/mod.rs | 11 + .../openshell-ocsf/src/validation/schema.rs | 132 ++ 77 files changed, 20094 insertions(+) create mode 100644 crates/openshell-ocsf/Cargo.toml create mode 100644 crates/openshell-ocsf/schemas/ocsf/README.md create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/VERSION create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/application_lifecycle.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/base_event.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/detection_finding.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/device_config_state_change.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/http_activity.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/network_activity.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/process_activity.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/ssh_activity.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/actor.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/attack.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/connection_info.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/container.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/device.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/evidences.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/finding_info.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/firewall_rule.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_request.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_response.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/metadata.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_endpoint.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_proxy.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/process.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/product.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/remediation.json create mode 100644 crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/url.json create mode 100644 crates/openshell-ocsf/src/builders/base.rs create mode 100644 crates/openshell-ocsf/src/builders/config.rs create mode 100644 crates/openshell-ocsf/src/builders/finding.rs create mode 100644 crates/openshell-ocsf/src/builders/http.rs create mode 100644 crates/openshell-ocsf/src/builders/lifecycle.rs create mode 100644 crates/openshell-ocsf/src/builders/mod.rs create mode 100644 crates/openshell-ocsf/src/builders/network.rs create mode 100644 crates/openshell-ocsf/src/builders/process.rs create mode 100644 crates/openshell-ocsf/src/builders/ssh.rs create mode 100644 crates/openshell-ocsf/src/enums/action.rs create mode 100644 crates/openshell-ocsf/src/enums/activity.rs create mode 100644 crates/openshell-ocsf/src/enums/auth.rs create mode 100644 crates/openshell-ocsf/src/enums/disposition.rs create mode 100644 crates/openshell-ocsf/src/enums/launch.rs create mode 100644 crates/openshell-ocsf/src/enums/mod.rs create mode 100644 crates/openshell-ocsf/src/enums/security.rs create mode 100644 crates/openshell-ocsf/src/enums/severity.rs create mode 100644 crates/openshell-ocsf/src/enums/status.rs create mode 100644 crates/openshell-ocsf/src/events/app_lifecycle.rs create mode 100644 crates/openshell-ocsf/src/events/base_event.rs create mode 100644 crates/openshell-ocsf/src/events/config_state_change.rs create mode 100644 crates/openshell-ocsf/src/events/detection_finding.rs create mode 100644 crates/openshell-ocsf/src/events/http_activity.rs create mode 100644 crates/openshell-ocsf/src/events/mod.rs create mode 100644 crates/openshell-ocsf/src/events/network_activity.rs create mode 100644 crates/openshell-ocsf/src/events/process_activity.rs create mode 100644 crates/openshell-ocsf/src/events/ssh_activity.rs create mode 100644 crates/openshell-ocsf/src/format/jsonl.rs create mode 100644 crates/openshell-ocsf/src/format/mod.rs create mode 100644 crates/openshell-ocsf/src/format/shorthand.rs create mode 100644 crates/openshell-ocsf/src/lib.rs create mode 100644 crates/openshell-ocsf/src/objects/attack.rs create mode 100644 crates/openshell-ocsf/src/objects/connection.rs create mode 100644 crates/openshell-ocsf/src/objects/container.rs create mode 100644 crates/openshell-ocsf/src/objects/device.rs create mode 100644 crates/openshell-ocsf/src/objects/endpoint.rs create mode 100644 crates/openshell-ocsf/src/objects/finding.rs create mode 100644 crates/openshell-ocsf/src/objects/firewall_rule.rs create mode 100644 crates/openshell-ocsf/src/objects/http.rs create mode 100644 crates/openshell-ocsf/src/objects/metadata.rs create mode 100644 crates/openshell-ocsf/src/objects/mod.rs create mode 100644 crates/openshell-ocsf/src/objects/process.rs create mode 100644 crates/openshell-ocsf/src/tracing_layers/event_bridge.rs create mode 100644 crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs create mode 100644 crates/openshell-ocsf/src/tracing_layers/mod.rs create mode 100644 crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs create mode 100644 crates/openshell-ocsf/src/validation/mod.rs create mode 100644 crates/openshell-ocsf/src/validation/schema.rs diff --git a/Cargo.lock b/Cargo.lock index 305c8a08..9d8247e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2882,6 +2882,18 @@ dependencies = [ "url", ] +[[package]] +name = "openshell-ocsf" +version = "0.0.0" +dependencies = [ + "chrono", + "serde", + "serde_json", + "serde_repr", + "tracing", + "tracing-subscriber", +] + [[package]] name = "openshell-policy" version = "0.0.0" diff --git a/crates/openshell-ocsf/Cargo.toml b/crates/openshell-ocsf/Cargo.toml new file mode 100644 index 00000000..14cc93ba --- /dev/null +++ b/crates/openshell-ocsf/Cargo.toml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "openshell-ocsf" +description = "OCSF v1.7.0 event types, formatters, and tracing layers for OpenShell sandbox logging" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +chrono = { version = "0.4", features = ["serde"] } +serde = { workspace = true } +serde_json = { workspace = true } +serde_repr = "0.1" +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[dev-dependencies] +tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } + +[lints] +workspace = true diff --git a/crates/openshell-ocsf/schemas/ocsf/README.md b/crates/openshell-ocsf/schemas/ocsf/README.md new file mode 100644 index 00000000..520325a0 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/README.md @@ -0,0 +1,53 @@ +# Vendored OCSF Schemas + +These schemas are vendored from the [OCSF Schema Server](https://schema.ocsf.io/) +for offline test validation. + +## Version + +- **OCSF v1.7.0** — fetched from `https://schema.ocsf.io/api/1.7.0/` + +## Contents + +### Classes (8) + +- `network_activity` [4001] +- `http_activity` [4002] +- `ssh_activity` [4007] +- `process_activity` [1007] +- `detection_finding` [2004] +- `application_lifecycle` [6002] +- `device_config_state_change` [5019] +- `base_event` [0] + +### Objects (17) + +- `metadata`, `network_endpoint`, `network_proxy`, `process`, `actor` +- `device`, `container`, `product`, `firewall_rule`, `finding_info` +- `evidences`, `http_request`, `http_response`, `url`, `attack` +- `remediation`, `connection_info` + +## Updating + +To update to a new OCSF version: + +```bash +VERSION=1.7.0 + +for class in network_activity http_activity ssh_activity process_activity \ + detection_finding application_lifecycle device_config_state_change base_event; do + curl -s "https://schema.ocsf.io/api/${VERSION}/classes/${class}" \ + | python3 -m json.tool > "classes/${class}.json" +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; do + curl -s "https://schema.ocsf.io/api/${VERSION}/objects/${object}" \ + | python3 -m json.tool > "objects/${object}.json" +done + +echo "${VERSION}" > VERSION +``` + +Then update `OCSF_VERSION` in `crates/openshell-ocsf/src/lib.rs` to match. diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/VERSION b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/VERSION new file mode 100644 index 00000000..bd8bf882 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/VERSION @@ -0,0 +1 @@ +1.7.0 diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/application_lifecycle.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/application_lifecycle.json new file mode 100644 index 00000000..6cbaf2e6 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/application_lifecycle.json @@ -0,0 +1,1047 @@ +{ + "attributes": [ + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "app": { + "type": "object_t", + "description": "The application that was affected by the lifecycle event. This also applies to self-updating application systems.", + "group": "primary", + "requirement": "required", + "caption": "Application", + "object_name": "Product", + "object_type": "product", + "_source": "application_lifecycle" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: Application Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "600203": { + "description": "Start the application.", + "caption": "Application Lifecycle: Start" + }, + "600206": { + "description": "Enable the application.", + "caption": "Application Lifecycle: Enable" + }, + "600200": { + "caption": "Application Lifecycle: Unknown" + }, + "600201": { + "description": "Install the application.", + "caption": "Application Lifecycle: Install" + }, + "600202": { + "description": "Remove the application.", + "caption": "Application Lifecycle: Remove" + }, + "600299": { + "caption": "Application Lifecycle: Other" + }, + "600204": { + "description": "Stop the application.", + "caption": "Application Lifecycle: Stop" + }, + "600205": { + "description": "Restart the application.", + "caption": "Application Lifecycle: Restart" + }, + "600207": { + "description": "Disable the application.", + "caption": "Application Lifecycle: Disable" + }, + "600208": { + "description": "Update the application.", + "caption": "Application Lifecycle: Update" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "application_lifecycle" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "6": { + "description": "Application Activity events report detailed information about the behavior of applications and services.", + "uid": 6, + "caption": "Application Activity" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "application_lifecycle" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: Application Lifecycle.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": "host", + "type": "object_t", + "description": "An addressable device, computer system or host.", + "group": "primary", + "requirement": "recommended", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "base_event" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Start the application.", + "caption": "Start" + }, + "6": { + "description": "Enable the application.", + "caption": "Enable" + }, + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Install the application.", + "caption": "Install" + }, + "2": { + "description": "Remove the application.", + "caption": "Remove" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Stop the application.", + "caption": "Stop" + }, + "5": { + "description": "Restart the application.", + "caption": "Restart" + }, + "7": { + "description": "Disable the application.", + "caption": "Disable" + }, + "8": { + "description": "Update the application.", + "caption": "Update" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "application_lifecycle", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "6002": { + "description": "Application Lifecycle events report installation, removal, start, stop of an application or service.", + "caption": "Application Lifecycle" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "application_lifecycle" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": "host", + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "primary", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "base_event" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "application_lifecycle", + "description": "Application Lifecycle events report installation, removal, start, stop of an application or service.", + "uid": 6002, + "extends": "application", + "category": "application", + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 6, + "caption": "Application Lifecycle", + "category_name": "Application Activity" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/base_event.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/base_event.json new file mode 100644 index 00000000..b9be86a7 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/base_event.json @@ -0,0 +1,964 @@ +{ + "attributes": [ + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "0": { + "caption": "Base Event: Unknown" + }, + "99": { + "caption": "Base Event: Other" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "base_event" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "0": { + "caption": "Uncategorized" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "base_event" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: Base Event.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": "host", + "type": "object_t", + "description": "An addressable device, computer system or host.", + "group": "primary", + "requirement": "recommended", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "base_event" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "base_event", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "0": { + "description": "The base event is a generic and concrete event. It also defines a set of attributes available in most event classes. As a generic event that does not belong to any event category, it could be used to log events that are not otherwise defined by the schema.", + "caption": "Base Event" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "base_event" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": "host", + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "primary", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "base_event" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "base_event", + "description": "The base event is a generic and concrete event. It also defines a set of attributes available in most event classes. As a generic event that does not belong to any event category, it could be used to log events that are not otherwise defined by the schema.", + "uid": 0, + "category": "other", + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control" + ], + "category_uid": 0, + "caption": "Base Event" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/detection_finding.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/detection_finding.json new file mode 100644 index 00000000..256f4db9 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/detection_finding.json @@ -0,0 +1,1397 @@ +{ + "attributes": [ + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "risk_level": { + "profile": null, + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "detection_finding", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time of the least recent event included in the finding.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "finding" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "anomaly_analyses": { + "type": "object_t", + "description": "Describes baseline information about normal activity patterns, along with any detected deviations or anomalies that triggered this finding.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Anomaly Analyses", + "object_name": "Anomaly Analysis", + "object_type": "anomaly_analysis", + "_source": "detection_finding" + } + }, + { + "confidence": { + "profile": null, + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "detection_finding", + "_sibling_of": "confidence_id" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "impact_score": { + "profile": null, + "type": "integer_t", + "description": "The impact as an integer value of the finding, valid range 0-100.", + "group": "context", + "requirement": "optional", + "caption": "Impact Score", + "type_name": "Integer", + "_source": "detection_finding" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "is_suspected_breach": { + "profile": "incident", + "type": "boolean_t", + "description": "A determination based on analytics as to whether a potential breach was found.", + "group": "context", + "requirement": "optional", + "caption": "Suspected Breach", + "type_name": "Boolean", + "_source": "finding" + } + }, + { + "priority": { + "profile": "incident", + "type": "string_t", + "description": "The priority, normalized to the caption of the priority_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Priority", + "type_name": "String", + "_source": "finding", + "_sibling_of": "priority_id" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": null, + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "detection_finding", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "ticket": { + "profile": "incident", + "type": "object_t", + "description": "The linked ticket in the ticketing system.", + "group": "context", + "requirement": "optional", + "caption": "Ticket", + "object_name": "Ticket", + "object_type": "ticket", + "@deprecated": { + "message": "Use tickets instead.", + "since": "1.5.0" + }, + "_source": "finding" + } + }, + { + "tickets": { + "profile": "incident", + "type": "object_t", + "description": "The associated ticket(s) in the ticketing system. Each ticket contains details like ticket ID, status, etc.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Tickets", + "object_name": "Ticket", + "object_type": "ticket", + "_source": "finding" + } + }, + { + "vendor_attributes": { + "type": "object_t", + "description": "The Vendor Attributes object can be used to represent values of attributes populated by the Vendor/Finding Provider. It can help distinguish between the vendor-provided values and consumer-updated values, of key attributes like severity_id.
The original finding producer should not populate this object. It should be populated by consuming systems that support data mutability.", + "group": "context", + "requirement": "optional", + "caption": "Vendor Attributes", + "object_name": "Vendor Attributes", + "object_type": "vendor_attributes", + "_source": "finding" + } + }, + { + "priority_id": { + "profile": "incident", + "type": "integer_t", + "enum": { + "3": { + "description": "Critical functionality or network access is interrupted, degraded or unusable, having a severe impact on services availability. No acceptable alternative is possible.", + "caption": "High" + }, + "0": { + "description": "No priority is assigned.", + "caption": "Unknown" + }, + "1": { + "description": "Application or personal procedure is unusable, where a workaround is available or a repair is possible.", + "caption": "Low" + }, + "2": { + "description": "Non-critical function or procedure is unusable or hard to use causing operational disruptions with no direct impact on a service's availability. A workaround is available.", + "caption": "Medium" + }, + "99": { + "description": "The priority is not normalized.", + "caption": "Other" + }, + "4": { + "description": "Interruption making a critical functionality inaccessible or a complete network interruption causing a severe impact on services availability. There is no possible alternative.", + "caption": "Critical" + } + }, + "description": "The normalized priority. Priority identifies the relative importance of the incident or finding. It is a measurement of urgency.", + "group": "context", + "requirement": "recommended", + "caption": "Priority ID", + "type_name": "Integer", + "sibling": "priority", + "_source": "finding" + } + }, + { + "vulnerabilities": { + "type": "object_t", + "description": "Describes vulnerabilities reported in a Detection Finding.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Vulnerabilities", + "object_name": "Vulnerability Details", + "object_type": "vulnerability", + "_source": "detection_finding" + } + }, + { + "risk_details": { + "profile": null, + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "detection_finding" + } + }, + { + "remediation": { + "type": "object_t", + "description": "Describes the recommended remediation steps to address identified issue(s).", + "group": "context", + "requirement": "optional", + "caption": "Remediation Guidance", + "object_name": "Remediation", + "object_type": "remediation", + "_source": "detection_finding" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The time of the most recent event included in the finding.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "finding" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: Findings.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": null, + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. For example, an activity_id of 'Create' could constitute an alertable signal and the value would be true, while 'Close' likely would not and either omit the attribute or set its value to false. Note that other events with the security_control profile may also be deemed alertable signals and may also carry is_alert = true attributes.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "detection_finding" + } + }, + { + "assignee_group": { + "profile": "incident", + "type": "object_t", + "description": "The details of the group assigned to an Incident.", + "group": "context", + "requirement": "optional", + "caption": "Assignee Group", + "object_name": "Group", + "object_type": "group", + "_source": "finding" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "200403": { + "description": "A finding was closed.", + "caption": "Detection Finding: Close" + }, + "200400": { + "caption": "Detection Finding: Unknown" + }, + "200401": { + "description": "A finding was created.", + "caption": "Detection Finding: Create" + }, + "200402": { + "description": "A finding was updated.", + "caption": "Detection Finding: Update" + }, + "200499": { + "caption": "Detection Finding: Other" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "detection_finding" + } + }, + { + "confidence_id": { + "profile": null, + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "detection_finding" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "2": { + "description": "Findings events report findings, detections, and possible resolutions of malware, anomalies, or other actions performed by security products.", + "uid": 2, + "caption": "Findings" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "detection_finding" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The normalized status of the Finding set by the consumer normalized to the caption of the status_id value. In the case of 'Other', it is defined by the source.", + "group": "context", + "requirement": "optional", + "caption": "Status", + "type_name": "String", + "_source": "finding", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "assignee": { + "profile": "incident", + "type": "object_t", + "description": "The details of the user assigned to an Incident.", + "group": "context", + "requirement": "optional", + "caption": "Assignee", + "object_name": "User", + "object_type": "user", + "_source": "finding" + } + }, + { + "malware": { + "profile": null, + "type": "object_t", + "description": "Describes malware reported in a Detection Finding.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "detection_finding" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "src_url": { + "profile": "incident", + "type": "url_t", + "description": "A Url link used to access the original incident.", + "group": "primary", + "requirement": "recommended", + "caption": "Source URL", + "type_name": "URL String", + "_source": "finding" + } + }, + { + "verdict": { + "profile": "incident", + "type": "string_t", + "description": "The verdict assigned to an Incident finding.", + "group": "primary", + "requirement": "recommended", + "caption": "Verdict", + "type_name": "String", + "_source": "finding", + "_sibling_of": "verdict_id" + } + }, + { + "impact_id": { + "profile": null, + "type": "integer_t", + "enum": { + "3": { + "description": "The magnitude of harm is high.", + "caption": "High" + }, + "0": { + "description": "The normalized impact is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The magnitude of harm is low.", + "caption": "Low" + }, + "2": { + "description": "The magnitude of harm is moderate.", + "caption": "Medium" + }, + "99": { + "description": "The impact is not mapped. See the impact attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The magnitude of harm is high and the scope is widespread.", + "caption": "Critical" + } + }, + "description": "The normalized impact of the incident or finding. Per NIST, this is the magnitude of harm that can be expected to result from the consequences of unauthorized disclosure, modification, destruction, or loss of information or information system availability.", + "group": "context", + "source": "impact value; impact level", + "references": [ + { + "description": "NIST SP 800-172 from FIPS 199", + "url": "https://doi.org/10.6028/NIST.FIPS.199" + }, + { + "description": "NIST Computer Security Resource Center", + "url": "https://doi.org/10.6028/NIST.FIPS.199" + } + ], + "requirement": "optional", + "caption": "Impact ID", + "type_name": "Integer", + "sibling": "impact", + "_source": "detection_finding" + } + }, + { + "confidence_score": { + "profile": null, + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "detection_finding" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "The Finding was reviewed, determined to be benign or a false positive and is now suppressed.", + "caption": "Suppressed" + }, + "6": { + "description": "The Finding was deleted. For example, it might have been created in error.", + "caption": "Deleted" + }, + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The Finding is new and yet to be reviewed.", + "caption": "New" + }, + "2": { + "description": "The Finding is under review.", + "caption": "In Progress" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The Finding was reviewed, remediated and is now considered resolved.", + "caption": "Resolved" + }, + "5": { + "description": "The Finding was archived.", + "caption": "Archived" + } + }, + "description": "The normalized status identifier of the Finding, set by the consumer.", + "group": "context", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "finding" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: Detection Finding.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "comment": { + "type": "string_t", + "description": "A user provided comment about the finding.", + "group": "context", + "requirement": "optional", + "caption": "Comment", + "type_name": "String", + "_source": "finding" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "evidences": { + "type": "object_t", + "description": "Describes various evidence artifacts associated to the activity/activities that triggered a security detection.", + "group": "primary", + "is_array": true, + "requirement": "recommended", + "caption": "Evidence Artifacts", + "object_name": "Evidence Artifacts", + "object_type": "evidences", + "_source": "detection_finding" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time of the most recent event included in the finding.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "finding" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": null, + "type": "object_t", + "description": "Describes the affected device/host. If applicable, it can be used in conjunction with Resource(s).

e.g. Specific details about an AWS EC2 instance, that is affected by the Finding.

", + "group": "context", + "requirement": "optional", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "finding" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "A finding was closed.", + "caption": "Close" + }, + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A finding was created.", + "caption": "Create" + }, + "2": { + "description": "A finding was updated.", + "caption": "Update" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the finding activity.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "finding", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": null, + "type": "object_t", + "description": "Describes details about malware scan job that triggered this Detection Finding.", + "group": "context", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "detection_finding" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "2004": { + "description": "A Detection Finding describes detections or alerts generated by security products using correlation engines, detection engines or other methodologies. Note: if the event producer is a security control, the security_control profile should be applied and its attacks information, if present, should be duplicated into the finding_info object.
Note: If the Finding is an incident, i.e. requires incident workflow, also apply the incident profile or aggregate this finding into an Incident Finding.", + "caption": "Detection Finding" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "detection_finding" + } + }, + { + "finding_info": { + "type": "object_t", + "description": "Describes the supporting information about a generated finding.", + "group": "primary", + "requirement": "required", + "caption": "Finding Information", + "object_name": "Finding Information", + "object_type": "finding_info", + "_source": "finding" + } + }, + { + "risk_score": { + "profile": null, + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "detection_finding" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "impact": { + "profile": null, + "type": "string_t", + "description": "The impact , normalized to the caption of the impact_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Impact", + "type_name": "String", + "_source": "detection_finding", + "_sibling_of": "impact_id" + } + }, + { + "resources": { + "type": "object_t", + "description": "Describes details about resources that were the target of the activity that triggered the finding.", + "group": "context", + "is_array": true, + "requirement": "recommended", + "caption": "Affected Resources", + "object_name": "Resource Details", + "object_type": "resource_details", + "_source": "detection_finding" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The finding activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "finding", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": "host", + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "primary", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "base_event" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "verdict_id": { + "profile": "incident", + "type": "integer_t", + "enum": { + "3": { + "description": "The incident can be disregarded as it is unimportant, an error or accident.", + "caption": "Disregard" + }, + "6": { + "description": "The incident is a test.", + "caption": "Test" + }, + "0": { + "description": "The type is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The incident is a false positive.", + "caption": "False Positive" + }, + "2": { + "description": "The incident is a true positive.", + "caption": "True Positive" + }, + "99": { + "description": "The type is not mapped. See the type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The incident is suspicious.", + "caption": "Suspicious" + }, + "5": { + "description": "The incident is benign.", + "caption": "Benign" + }, + "7": { + "description": "The incident has insufficient data to make a verdict.", + "caption": "Insufficient Data" + }, + "8": { + "description": "The incident is a security risk.", + "caption": "Security Risk" + }, + "9": { + "description": "The incident remediation or required actions are managed externally.", + "caption": "Managed Externally" + }, + "10": { + "description": "The incident is a duplicate.", + "caption": "Duplicate" + } + }, + "description": "The normalized verdict of an Incident.", + "group": "primary", + "requirement": "recommended", + "caption": "Verdict ID", + "type_name": "Integer", + "sibling": "verdict", + "_source": "finding" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The time of the least recent event included in the finding.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "finding" + } + } + ], + "name": "detection_finding", + "description": "A Detection Finding describes detections or alerts generated by security products using correlation engines, detection engines or other methodologies. Note: if the event producer is a security control, the security_control profile should be applied and its attacks information, if present, should be duplicated into the finding_info object.
Note: If the Finding is an incident, i.e. requires incident workflow, also apply the incident profile or aggregate this finding into an Incident Finding.", + "uid": 2004, + "extends": "finding", + "category": "findings", + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "incident", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 2, + "caption": "Detection Finding", + "category_name": "Findings" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/device_config_state_change.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/device_config_state_change.json new file mode 100644 index 00000000..9fda4882 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/device_config_state_change.json @@ -0,0 +1,1137 @@ +{ + "attributes": [ + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "prev_security_level": { + "type": "string_t", + "description": "The previous security level of the entity", + "group": "primary", + "requirement": "recommended", + "caption": "Previous Security Level", + "type_name": "String", + "_source": "device_config_state_change", + "_sibling_of": "prev_security_level_id" + } + }, + { + "security_level_id": { + "type": "integer_t", + "enum": { + "3": { + "caption": "Compromised" + }, + "0": { + "caption": "Unknown" + }, + "1": { + "caption": "Secure" + }, + "2": { + "caption": "At Risk" + }, + "99": { + "description": "The security level is not mapped. See the security_level attribute, which contains data source specific values.", + "caption": "Other" + } + }, + "description": "The current security level of the entity", + "group": "primary", + "requirement": "recommended", + "caption": "Security Level ID", + "type_name": "Integer", + "sibling": "security_level", + "_source": "device_config_state_change" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: Discovery.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "501900": { + "caption": "Device Config State Change: Unknown" + }, + "501901": { + "description": "The discovered information is via a log.", + "caption": "Device Config State Change: Log" + }, + "501902": { + "description": "The discovered information is via a collection process.", + "caption": "Device Config State Change: Collect" + }, + "501999": { + "caption": "Device Config State Change: Other" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "device_config_state_change" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "security_states": { + "type": "object_t", + "description": "The current security states of the device.", + "group": "primary", + "is_array": true, + "requirement": "recommended", + "caption": "Security States", + "object_name": "Security State", + "object_type": "security_state", + "_source": "device_config_state_change" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "5": { + "description": "Discovery events report the existence and state of devices, files, configurations, processes, registry keys, and other objects.", + "uid": 5, + "caption": "Discovery" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "device_config_state_change" + } + }, + { + "prev_security_level_id": { + "type": "integer_t", + "enum": { + "3": { + "caption": "Compromised" + }, + "0": { + "caption": "Unknown" + }, + "1": { + "caption": "Secure" + }, + "2": { + "caption": "At Risk" + }, + "99": { + "description": "The security level is not mapped. See the prev_security_level attribute, which contains data source specific values.", + "caption": "Other" + } + }, + "description": "The previous security level of the entity", + "group": "primary", + "requirement": "recommended", + "caption": "Previous Security Level ID", + "type_name": "Integer", + "sibling": "prev_security_level", + "_source": "device_config_state_change" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "state_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The Config Change state is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Config State Changed to Disabled.", + "caption": "Disabled" + }, + "2": { + "description": "Config State Changed to Enabled.", + "caption": "Enabled" + }, + "99": { + "description": "The Config Change is not mapped. See the state attribute, which contains data source specific values.", + "caption": "Other" + } + }, + "description": "The Config Change State of the managed entity.", + "requirement": "recommended", + "caption": "Config Change State ID", + "type_name": "Integer", + "sibling": "state", + "_source": "device_config_state_change" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: Device Config State Change.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "state": { + "type": "string_t", + "description": "The Config Change Stat, normalized to the caption of the state_id value. In the case of 'Other', it is defined by the source.", + "requirement": "optional", + "caption": "Config Change State", + "type_name": "String", + "_source": "device_config_state_change", + "_sibling_of": "state_id" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": null, + "type": "object_t", + "description": "The device that is impacted by the state change.", + "group": "primary", + "requirement": "required", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "device_config_state_change" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The discovered information is via a log.", + "caption": "Log" + }, + "2": { + "description": "The discovered information is via a collection process.", + "caption": "Collect" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "discovery", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "5019": { + "description": "Device Config State Change events report state changes that impact the security of the device.", + "caption": "Device Config State Change" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "device_config_state_change" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": null, + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "context", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "device_config_state_change" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "security_level": { + "type": "string_t", + "description": "The current security level of the entity", + "group": "primary", + "requirement": "recommended", + "caption": "Security Level", + "type_name": "String", + "_source": "device_config_state_change", + "_sibling_of": "security_level_id" + } + }, + { + "prev_security_states": { + "type": "object_t", + "description": "The previous security states of the device.", + "group": "primary", + "is_array": true, + "requirement": "recommended", + "caption": "Previous Security States", + "object_name": "Security State", + "object_type": "security_state", + "_source": "device_config_state_change" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "device_config_state_change", + "description": "Device Config State Change events report state changes that impact the security of the device.", + "uid": 5019, + "extends": "discovery", + "category": "discovery", + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 5, + "caption": "Device Config State Change", + "category_name": "Discovery" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/http_activity.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/http_activity.json new file mode 100644 index 00000000..55a8760d --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/http_activity.json @@ -0,0 +1,1380 @@ +{ + "attributes": [ + { + "proxy_http_request": { + "profile": "network_proxy", + "type": "object_t", + "description": "The HTTP Request from the proxy server to the remote server.", + "group": "context", + "requirement": "optional", + "caption": "Proxy HTTP Request", + "object_name": "HTTP Request", + "object_type": "http_request", + "_source": "network" + } + }, + { + "proxy_endpoint": { + "profile": "network_proxy", + "type": "object_t", + "description": "The proxy (server) in a network connection.", + "group": "context", + "requirement": "optional", + "caption": "Proxy Endpoint", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "_source": "network" + } + }, + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "observation_point": { + "type": "string_t", + "description": "Indicates whether the source network endpoint, destination network endpoint, or neither served as the observation point for the activity. The value is normalized to the caption of the observation_point_id.", + "requirement": "optional", + "caption": "Observation Point", + "type_name": "String", + "_source": "network", + "_sibling_of": "observation_point_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "observation_point_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Neither the source nor destination network endpoint is the observation point.", + "caption": "Neither" + }, + "0": { + "description": "The observation point is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The source network endpoint is the observation point.", + "caption": "Source" + }, + "2": { + "description": "The destination network endpoint is the observation point.", + "caption": "Destination" + }, + "99": { + "description": "The observation point is not mapped. See the observation_point attribute for a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Both the source and destination network endpoint are the observation point. This typically occurs in localhost or internal communications where the source and destination are the same endpoint, often resulting in a connection_info.direction of Local.", + "caption": "Both" + } + }, + "description": "The normalized identifier of the observation point. The observation point identifier indicates whether the source network endpoint, destination network endpoint, or neither served as the observation point for the activity.", + "requirement": "optional", + "caption": "Observation Point ID", + "type_name": "Integer", + "sibling": "observation_point", + "_source": "network" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "connection_info": { + "type": "object_t", + "description": "The network connection information.", + "group": "primary", + "requirement": "recommended", + "caption": "Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "network" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "ja4_fingerprint_list": { + "type": "object_t", + "description": "A list of the JA4+ network fingerprints.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "JA4+ Fingerprints", + "object_name": "JA4+ Fingerprint", + "object_type": "ja4_fingerprint", + "_source": "network" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "dst_endpoint": { + "type": "object_t", + "description": "The responder (server) in a network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Destination Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "network" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: Network Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "http_status": { + "type": "integer_t", + "description": "The Hypertext Transfer Protocol (HTTP) status code returned to the client.", + "group": "primary", + "requirement": "recommended", + "caption": "HTTP Status", + "type_name": "Integer", + "@deprecated": { + "message": "Use the http_response.code attribute instead.", + "since": "1.1.0" + }, + "_source": "http_activity" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "400203": { + "description": "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data.", + "caption": "HTTP Activity: Get" + }, + "400206": { + "description": "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.", + "caption": "HTTP Activity: Post" + }, + "400200": { + "caption": "HTTP Activity: Unknown" + }, + "400201": { + "description": "The CONNECT method establishes a tunnel to the server identified by the target resource.", + "caption": "HTTP Activity: Connect" + }, + "400202": { + "description": "The DELETE method deletes the specified resource.", + "caption": "HTTP Activity: Delete" + }, + "400299": { + "caption": "HTTP Activity: Other" + }, + "400204": { + "description": "The HEAD method asks for a response identical to a GET request, but without the response body.", + "caption": "HTTP Activity: Head" + }, + "400205": { + "description": "The OPTIONS method describes the communication options for the target resource.", + "caption": "HTTP Activity: Options" + }, + "400207": { + "description": "The PUT method replaces all current representations of the target resource with the request payload.", + "caption": "HTTP Activity: Put" + }, + "400208": { + "description": "The TRACE method performs a message loop-back test along the path to the target resource.", + "caption": "HTTP Activity: Trace" + }, + "400209": { + "description": "The PATCH method applies partial modifications to a resource.", + "caption": "HTTP Activity: Patch" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "http_activity" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "4": { + "description": "Network Activity events.", + "uid": 4, + "caption": "Network Activity" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "http_activity" + } + }, + { + "proxy_traffic": { + "profile": "network_proxy", + "type": "object_t", + "description": "The network traffic refers to the amount of data moving across a network, from proxy to remote server at a given point of time.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "load_balancer": { + "profile": "load_balancer", + "type": "object_t", + "description": "The Load Balancer object contains information related to the device that is distributing incoming traffic to specified destinations.", + "group": "primary", + "requirement": "recommended", + "caption": "Load Balancer", + "object_name": "Load Balancer", + "object_type": "load_balancer", + "_source": "network" + } + }, + { + "app_name": { + "type": "string_t", + "description": "The name of the application associated with the event or object.", + "group": "context", + "requirement": "optional", + "caption": "Application Name", + "type_name": "String", + "_source": "network" + } + }, + { + "src_endpoint": { + "type": "object_t", + "description": "The initiator (client) of the network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Source Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "network" + } + }, + { + "proxy_tls": { + "profile": "network_proxy", + "type": "object_t", + "description": "The TLS protocol negotiated between the proxy server and the remote server.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "network" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "http_request": { + "type": "object_t", + "description": "The HTTP Request Object documents attributes of a request made to a web server.", + "group": "primary", + "requirement": "recommended", + "caption": "HTTP Request", + "object_name": "HTTP Request", + "object_type": "http_request", + "_source": "http_activity" + } + }, + { + "traffic": { + "type": "object_t", + "description": "The network traffic for this observation period. Use when reporting: (1) delta values (bytes/packets transferred since the last observation), (2) instantaneous measurements at a specific point in time, or (3) standalone single-event metrics. This attribute represents a point-in-time measurement or incremental change, not a running total. For accumulated totals across multiple observations or the lifetime of a flow, use cumulative_traffic instead.", + "group": "primary", + "requirement": "recommended", + "caption": "Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "http_cookies": { + "type": "object_t", + "description": "The cookies object describes details about HTTP cookies", + "group": "primary", + "is_array": true, + "requirement": "recommended", + "caption": "HTTP Cookies", + "object_name": "HTTP Cookie", + "object_type": "http_cookie", + "_source": "http_activity" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "proxy": { + "type": "object_t", + "description": "The proxy (server) in a network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Proxy", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "@deprecated": { + "message": "Use the proxy_endpoint attribute instead.", + "since": "1.1.0" + }, + "_source": "network" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "file": { + "type": "object_t", + "description": "The file that is the target of the HTTP activity.", + "group": "context", + "requirement": "optional", + "caption": "File", + "object_name": "File", + "object_type": "file", + "_source": "http_activity" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: HTTP Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "proxy_connection_info": { + "profile": "network_proxy", + "type": "object_t", + "description": "The connection information from the proxy server to the remote server.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "network" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": "host", + "type": "object_t", + "description": "An addressable device, computer system or host.", + "group": "primary", + "requirement": "recommended", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "base_event" + } + }, + { + "http_response": { + "type": "object_t", + "description": "The HTTP Response from a web server to a requester.", + "group": "primary", + "requirement": "recommended", + "caption": "HTTP Response", + "object_name": "HTTP Response", + "object_type": "http_response", + "_source": "http_activity" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data.", + "caption": "Get" + }, + "6": { + "description": "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.", + "caption": "Post" + }, + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The CONNECT method establishes a tunnel to the server identified by the target resource.", + "caption": "Connect" + }, + "2": { + "description": "The DELETE method deletes the specified resource.", + "caption": "Delete" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The HEAD method asks for a response identical to a GET request, but without the response body.", + "caption": "Head" + }, + "5": { + "description": "The OPTIONS method describes the communication options for the target resource.", + "caption": "Options" + }, + "7": { + "description": "The PUT method replaces all current representations of the target resource with the request payload.", + "caption": "Put" + }, + "8": { + "description": "The TRACE method performs a message loop-back test along the path to the target resource.", + "caption": "Trace" + }, + "9": { + "description": "The PATCH method applies partial modifications to a resource.", + "caption": "Patch" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "http_activity", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "4002": { + "description": "HTTP Activity events report HTTP connection and traffic information.", + "caption": "HTTP Activity" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "http_activity" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "trace": { + "profile": "trace", + "type": "object_t", + "description": "The trace object contains information about distributed traces which are critical to observability and describe how requests move through a system, capturing each step's timing and status.", + "group": "primary", + "requirement": "recommended", + "caption": "Trace", + "object_name": "Trace", + "object_type": "trace", + "_source": "http_activity" + } + }, + { + "proxy_http_response": { + "profile": "network_proxy", + "type": "object_t", + "description": "The HTTP Response from the remote server to the proxy server.", + "group": "context", + "requirement": "optional", + "caption": "Proxy HTTP Response", + "object_name": "HTTP Response", + "object_type": "http_response", + "_source": "network" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": "host", + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "primary", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "base_event" + } + }, + { + "tls": { + "type": "object_t", + "description": "The Transport Layer Security (TLS) attributes.", + "group": "context", + "requirement": "optional", + "caption": "TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "network" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "cumulative_traffic": { + "type": "object_t", + "description": "The cumulative (running total) network traffic aggregated from the start of a flow or session. Use when reporting: (1) total accumulated bytes/packets since flow initiation, (2) combined aggregation models where both incremental deltas and running totals are reported together (populate both traffic for the delta and this attribute for the cumulative total), or (3) final summary metrics when a long-lived connection closes. This represents the sum of all activity from flow start to the current observation, not a delta or point-in-time value.", + "group": "context", + "requirement": "optional", + "caption": "Cumulative Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "http_activity", + "description": "HTTP Activity events report HTTP connection and traffic information.", + "uid": 4002, + "extends": "network", + "category": "network", + "constraints": { + "at_least_one": [ + "http_request", + "http_response" + ] + }, + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "network_proxy", + "load_balancer", + "trace", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 4, + "caption": "HTTP Activity", + "category_name": "Network Activity" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/network_activity.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/network_activity.json new file mode 100644 index 00000000..1e1f5c9e --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/network_activity.json @@ -0,0 +1,1309 @@ +{ + "attributes": [ + { + "proxy_http_request": { + "profile": "network_proxy", + "type": "object_t", + "description": "The HTTP Request from the proxy server to the remote server.", + "group": "context", + "requirement": "optional", + "caption": "Proxy HTTP Request", + "object_name": "HTTP Request", + "object_type": "http_request", + "_source": "network" + } + }, + { + "proxy_endpoint": { + "profile": "network_proxy", + "type": "object_t", + "description": "The proxy (server) in a network connection.", + "group": "context", + "requirement": "optional", + "caption": "Proxy Endpoint", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "_source": "network" + } + }, + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "observation_point": { + "type": "string_t", + "description": "Indicates whether the source network endpoint, destination network endpoint, or neither served as the observation point for the activity. The value is normalized to the caption of the observation_point_id.", + "requirement": "optional", + "caption": "Observation Point", + "type_name": "String", + "_source": "network", + "_sibling_of": "observation_point_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "observation_point_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Neither the source nor destination network endpoint is the observation point.", + "caption": "Neither" + }, + "0": { + "description": "The observation point is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The source network endpoint is the observation point.", + "caption": "Source" + }, + "2": { + "description": "The destination network endpoint is the observation point.", + "caption": "Destination" + }, + "99": { + "description": "The observation point is not mapped. See the observation_point attribute for a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Both the source and destination network endpoint are the observation point. This typically occurs in localhost or internal communications where the source and destination are the same endpoint, often resulting in a connection_info.direction of Local.", + "caption": "Both" + } + }, + "description": "The normalized identifier of the observation point. The observation point identifier indicates whether the source network endpoint, destination network endpoint, or neither served as the observation point for the activity.", + "requirement": "optional", + "caption": "Observation Point ID", + "type_name": "Integer", + "sibling": "observation_point", + "_source": "network" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "connection_info": { + "type": "object_t", + "description": "The network connection information.", + "group": "primary", + "requirement": "recommended", + "caption": "Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "network" + } + }, + { + "is_src_dst_assignment_known": { + "type": "boolean_t", + "description": "true denotes that src_endpoint and dst_endpoint correctly identify the initiator and responder respectively. false denotes that the event source has arbitrarily assigned one peer to src_endpoint and the other to dst_endpoint, in other words that initiator and responder are not being asserted. This can occur, for example, when the event source is a network appliance that has not observed the initiation of a given connection. In the absence of this attribute, interpretation of the initiator and responder is implementation-specific.", + "group": "primary", + "requirement": "recommended", + "caption": "Source/Destination Assignment Known", + "type_name": "Boolean", + "_source": "network_activity" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "url": { + "type": "object_t", + "description": "The URL details relevant to the network traffic.", + "group": "primary", + "requirement": "recommended", + "caption": "URL", + "object_name": "Uniform Resource Locator", + "object_type": "url", + "_source": "network_activity" + } + }, + { + "ja4_fingerprint_list": { + "type": "object_t", + "description": "A list of the JA4+ network fingerprints.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "JA4+ Fingerprints", + "object_name": "JA4+ Fingerprint", + "object_type": "ja4_fingerprint", + "_source": "network" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "dst_endpoint": { + "type": "object_t", + "description": "The responder of the network connection. In some contexts an event source cannot correctly identify the responder. Refer to is_src_dst_assignment_known for certainty.", + "group": "primary", + "requirement": "recommended", + "caption": "Destination Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "network_activity" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: Network Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "400103": { + "description": "The network connection was abnormally terminated or closed by a middle device like firewalls.", + "caption": "Network Activity: Reset" + }, + "400106": { + "description": "Network traffic report.", + "caption": "Network Activity: Traffic" + }, + "400100": { + "caption": "Network Activity: Unknown" + }, + "400101": { + "description": "A new network connection was opened.", + "caption": "Network Activity: Open" + }, + "400102": { + "description": "The network connection was closed.", + "caption": "Network Activity: Close" + }, + "400199": { + "caption": "Network Activity: Other" + }, + "400104": { + "description": "The network connection failed. For example a connection timeout or no route to host.", + "caption": "Network Activity: Fail" + }, + "400105": { + "description": "The network connection was refused. For example an attempt to connect to a server port which is not open.", + "caption": "Network Activity: Refuse" + }, + "400107": { + "description": "A network endpoint began listening for new network connections.", + "caption": "Network Activity: Listen" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "network_activity" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "4": { + "description": "Network Activity events.", + "uid": 4, + "caption": "Network Activity" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "network_activity" + } + }, + { + "proxy_traffic": { + "profile": "network_proxy", + "type": "object_t", + "description": "The network traffic refers to the amount of data moving across a network, from proxy to remote server at a given point of time.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "load_balancer": { + "profile": "load_balancer", + "type": "object_t", + "description": "The Load Balancer object contains information related to the device that is distributing incoming traffic to specified destinations.", + "group": "primary", + "requirement": "recommended", + "caption": "Load Balancer", + "object_name": "Load Balancer", + "object_type": "load_balancer", + "_source": "network" + } + }, + { + "app_name": { + "type": "string_t", + "description": "The name of the application associated with the event or object.", + "group": "context", + "requirement": "optional", + "caption": "Application Name", + "type_name": "String", + "_source": "network" + } + }, + { + "src_endpoint": { + "type": "object_t", + "description": " The initiator of the network connection. In some contexts an event source cannot correctly identify the initiator. Refer to is_src_dst_assignment_known for certainty.", + "group": "primary", + "requirement": "recommended", + "caption": "Source Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "network_activity" + } + }, + { + "proxy_tls": { + "profile": "network_proxy", + "type": "object_t", + "description": "The TLS protocol negotiated between the proxy server and the remote server.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "network" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "traffic": { + "type": "object_t", + "description": "The network traffic for this observation period. Use when reporting: (1) delta values (bytes/packets transferred since the last observation), (2) instantaneous measurements at a specific point in time, or (3) standalone single-event metrics. This attribute represents a point-in-time measurement or incremental change, not a running total. For accumulated totals across multiple observations or the lifetime of a flow, use cumulative_traffic instead.", + "group": "primary", + "requirement": "recommended", + "caption": "Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "proxy": { + "type": "object_t", + "description": "The proxy (server) in a network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Proxy", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "@deprecated": { + "message": "Use the proxy_endpoint attribute instead.", + "since": "1.1.0" + }, + "_source": "network" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: Network Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "proxy_connection_info": { + "profile": "network_proxy", + "type": "object_t", + "description": "The connection information from the proxy server to the remote server.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "network" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": "host", + "type": "object_t", + "description": "An addressable device, computer system or host.", + "group": "primary", + "requirement": "recommended", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "base_event" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "The network connection was abnormally terminated or closed by a middle device like firewalls.", + "caption": "Reset" + }, + "6": { + "description": "Network traffic report.", + "caption": "Traffic" + }, + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A new network connection was opened.", + "caption": "Open" + }, + "2": { + "description": "The network connection was closed.", + "caption": "Close" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The network connection failed. For example a connection timeout or no route to host.", + "caption": "Fail" + }, + "5": { + "description": "The network connection was refused. For example an attempt to connect to a server port which is not open.", + "caption": "Refuse" + }, + "7": { + "description": "A network endpoint began listening for new network connections.", + "caption": "Listen" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "network_activity", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "4001": { + "description": "Network Activity events report network connection and traffic activity.", + "caption": "Network Activity" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "network_activity" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "proxy_http_response": { + "profile": "network_proxy", + "type": "object_t", + "description": "The HTTP Response from the remote server to the proxy server.", + "group": "context", + "requirement": "optional", + "caption": "Proxy HTTP Response", + "object_name": "HTTP Response", + "object_type": "http_response", + "_source": "network" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": "host", + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "primary", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "base_event" + } + }, + { + "tls": { + "type": "object_t", + "description": "The Transport Layer Security (TLS) attributes.", + "group": "context", + "requirement": "optional", + "caption": "TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "network" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "cumulative_traffic": { + "type": "object_t", + "description": "The cumulative (running total) network traffic aggregated from the start of a flow or session. Use when reporting: (1) total accumulated bytes/packets since flow initiation, (2) combined aggregation models where both incremental deltas and running totals are reported together (populate both traffic for the delta and this attribute for the cumulative total), or (3) final summary metrics when a long-lived connection closes. This represents the sum of all activity from flow start to the current observation, not a delta or point-in-time value.", + "group": "context", + "requirement": "optional", + "caption": "Cumulative Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "network_activity", + "description": "Network Activity events report network connection and traffic activity.", + "uid": 4001, + "extends": "network", + "category": "network", + "constraints": { + "at_least_one": [ + "dst_endpoint", + "src_endpoint" + ] + }, + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "network_proxy", + "load_balancer", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 4, + "caption": "Network Activity", + "category_name": "Network Activity" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/process_activity.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/process_activity.json new file mode 100644 index 00000000..beb7655d --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/process_activity.json @@ -0,0 +1,1193 @@ +{ + "attributes": [ + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "launch_type": { + "type": "string_t", + "description": "The specific type of Launch activity, normalized to the caption of the launch_type_id value. In the case of Other it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Launch Type", + "type_name": "String", + "_source": "process_activity", + "_sibling_of": "launch_type_id" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "requested_permissions": { + "type": "integer_t", + "description": "The permissions mask that was requested by the process.", + "group": "primary", + "requirement": "recommended", + "caption": "Requested Permissions", + "type_name": "Integer", + "_source": "process_activity" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "injection_type": { + "type": "string_t", + "description": "The process injection method, normalized to the caption of the injection_type_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Injection Type", + "type_name": "String", + "_source": "process_activity", + "_sibling_of": "injection_type_id" + } + }, + { + "launch_type_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Denotes that the Launch event represents the \"exec\" step of Unix process creation, where a process replaces its executable image, command line, and environment. WSL1 pico processes on Windows also use the 2-step Unix model.", + "caption": "Exec" + }, + "0": { + "description": "The launch type is unknown or not specified.", + "caption": "Unknown" + }, + "1": { + "description": "Denotes that the Launch event represents atomic creation of a new process on Windows. This launch type ID may also be used to represent both steps of Unix process creation in a single Launch event.", + "caption": "Spawn" + }, + "2": { + "description": "Denotes that the Launch event represents the \"fork\" step of Unix process creation, where a process creates a clone of itself in a parent-child relationship. WSL1 pico processes on Windows also use the 2-step Unix model.", + "caption": "Fork" + }, + "99": { + "description": "The launch type is not mapped. See the launch_type attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier for the specific type of Launch activity.", + "group": "primary", + "references": [ + { + "description": "fork() man page", + "url": "https://www.man7.org/linux/man-pages/man2/fork.2.html" + }, + { + "description": "execve() man page", + "url": "https://www.man7.org/linux/man-pages/man2/execve.2.html" + } + ], + "requirement": "recommended", + "caption": "Launch Type ID", + "type_name": "Integer", + "sibling": "launch_type", + "_source": "process_activity" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "injection_type_id": { + "type": "integer_t", + "enum": { + "3": { + "caption": "Queue APC" + }, + "0": { + "description": "The injection type is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Remote Thread" + }, + "2": { + "caption": "Load Library" + }, + "99": { + "description": "The injection type is not mapped. See the injection_type attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the process injection method.", + "group": "primary", + "requirement": "recommended", + "caption": "Injection Type ID", + "type_name": "Integer", + "sibling": "injection_type", + "_source": "process_activity" + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "actual_permissions": { + "type": "integer_t", + "description": "The permissions that were granted to the process in a platform-native format.", + "group": "primary", + "requirement": "recommended", + "caption": "Actual Permissions", + "type_name": "Integer", + "_source": "process_activity" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: System Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "100703": { + "description": "A request by the actor to obtain a handle or descriptor to a process with the aim of performing further actions upon that process. The target is usually a different process but this activity can also be reflexive.", + "caption": "Process Activity: Open" + }, + "100700": { + "caption": "Process Activity: Unknown" + }, + "100701": { + "description": "A request by the actor to launch another process. Refer to the launch_type_id attribute for details of the specific launch type.", + "caption": "Process Activity: Launch" + }, + "100702": { + "description": "A request by the actor to terminate a process. This activity is most commonly reflexive, this being the case when a process exits at its own initiation. Note too that Windows security products cannot always identify the actor in the case of inter-process termination. In this case, actor.process and process refer to the exiting process, i.e. indistinguishable from the reflexive case.", + "caption": "Process Activity: Terminate" + }, + "100799": { + "caption": "Process Activity: Other" + }, + "100704": { + "description": "A request by the actor to execute code within the context of a process. The target is usually a different process but this activity can also be reflexive. Refer to the injection_type_id attribute for details of the specific injection type.", + "references": [ + { + "description": "Guidance on the use of \"Module Activity: Load\" and \"Process Activity: Inject\".", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/faqs/schema-faq.md#when-should-i-use-a-module-activity-load-event-and-when-should-i-use-a-process-activity-inject-event" + } + ], + "caption": "Process Activity: Inject" + }, + "100705": { + "description": "A request by the actor to change its user identity by invoking the setuid() system call. Common programs like su and sudo use this mechanism. Note that the impersonation mechanism on Windows is not directly equivalent because it acts at the thread level.", + "caption": "Process Activity: Set User ID" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "process_activity" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "1": { + "description": "System Activity events.", + "uid": 1, + "caption": "System Activity" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "process_activity" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "exit_code": { + "type": "integer_t", + "description": "The exit code reported by a process when it terminates. The convention is that zero indicates success and any non-zero exit code indicates that some error occurred.", + "group": "primary", + "requirement": "recommended", + "caption": "Exit Code", + "type_name": "Integer", + "_source": "process_activity" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "module": { + "type": "object_t", + "description": "The module that was injected by the actor process.", + "group": "primary", + "requirement": "recommended", + "caption": "Module", + "object_name": "Module", + "object_type": "module", + "_source": "process_activity" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: Process Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "process": { + "type": "object_t", + "description": "The process that was launched, injected into, opened, or terminated.", + "group": "primary", + "requirement": "required", + "caption": "Process", + "object_name": "Process", + "object_type": "process", + "_source": "process_activity" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": null, + "type": "object_t", + "description": "An addressable device, computer system or host.", + "group": "primary", + "requirement": "required", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "system" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "A request by the actor to obtain a handle or descriptor to a process with the aim of performing further actions upon that process. The target is usually a different process but this activity can also be reflexive.", + "caption": "Open" + }, + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A request by the actor to launch another process. Refer to the launch_type_id attribute for details of the specific launch type.", + "caption": "Launch" + }, + "2": { + "description": "A request by the actor to terminate a process. This activity is most commonly reflexive, this being the case when a process exits at its own initiation. Note too that Windows security products cannot always identify the actor in the case of inter-process termination. In this case, actor.process and process refer to the exiting process, i.e. indistinguishable from the reflexive case.", + "caption": "Terminate" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A request by the actor to execute code within the context of a process. The target is usually a different process but this activity can also be reflexive. Refer to the injection_type_id attribute for details of the specific injection type.", + "references": [ + { + "description": "Guidance on the use of \"Module Activity: Load\" and \"Process Activity: Inject\".", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/faqs/schema-faq.md#when-should-i-use-a-module-activity-load-event-and-when-should-i-use-a-process-activity-inject-event" + } + ], + "caption": "Inject" + }, + "5": { + "description": "A request by the actor to change its user identity by invoking the setuid() system call. Common programs like su and sudo use this mechanism. Note that the impersonation mechanism on Windows is not directly equivalent because it acts at the thread level.", + "caption": "Set User ID" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "references": [ + { + "description": "setuid() man page", + "url": "https://www.man7.org/linux/man-pages/man2/setuid.2.html" + } + ], + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "process_activity", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "1007": { + "description": "Process Activity events report when a process launches, injects, opens or terminates another process, successful or otherwise.", + "caption": "Process Activity" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "process_activity" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": null, + "type": "object_t", + "description": "The actor that performed the activity on the target process. For example, the process that started a new process or injected code into another process.", + "group": "primary", + "requirement": "required", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "process_activity" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "process_activity", + "description": "Process Activity events report when a process launches, injects, opens or terminates another process, successful or otherwise.", + "uid": 1007, + "extends": "system", + "category": "system", + "associations": { + "device": [ + "actor.user" + ], + "actor.user": [ + "device" + ] + }, + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 1, + "caption": "Process Activity", + "category_name": "System Activity" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/ssh_activity.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/ssh_activity.json new file mode 100644 index 00000000..0017b737 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/ssh_activity.json @@ -0,0 +1,1391 @@ +{ + "attributes": [ + { + "proxy_http_request": { + "profile": "network_proxy", + "type": "object_t", + "description": "The HTTP Request from the proxy server to the remote server.", + "group": "context", + "requirement": "optional", + "caption": "Proxy HTTP Request", + "object_name": "HTTP Request", + "object_type": "http_request", + "_source": "network" + } + }, + { + "proxy_endpoint": { + "profile": "network_proxy", + "type": "object_t", + "description": "The proxy (server) in a network connection.", + "group": "context", + "requirement": "optional", + "caption": "Proxy Endpoint", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "_source": "network" + } + }, + { + "server_hassh": { + "type": "object_t", + "description": "The Server HASSH fingerprinting object.", + "group": "primary", + "requirement": "recommended", + "caption": "Server HASSH", + "object_name": "HASSH", + "object_type": "hassh", + "_source": "ssh_activity" + } + }, + { + "severity": { + "type": "string_t", + "description": "The event/finding severity, normalized to the caption of the severity_id value. In the case of 'Other', it is defined by the source.", + "group": "classification", + "requirement": "optional", + "caption": "Severity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "severity_id" + } + }, + { + "observation_point": { + "type": "string_t", + "description": "Indicates whether the source network endpoint, destination network endpoint, or neither served as the observation point for the activity. The value is normalized to the caption of the observation_point_id.", + "requirement": "optional", + "caption": "Observation Point", + "type_name": "String", + "_source": "network", + "_sibling_of": "observation_point_id" + } + }, + { + "risk_level": { + "profile": "security_control", + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "risk_level_id" + } + }, + { + "status_code": { + "type": "string_t", + "description": "The event status code, as reported by the event source.

For example, in a Windows Failed Authentication event, this would be the value of 'Failure Code', e.g. 0x18.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Code", + "type_name": "String", + "_source": "base_event" + } + }, + { + "start_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "osint": { + "profile": "osint", + "type": "object_t", + "description": "The OSINT (Open Source Intelligence) object contains details related to an indicator such as the indicator itself, related indicators, geolocation, registrar information, subdomains, analyst commentary, and other contextual information. This information can be used to further enrich a detection or finding by providing decisioning support to other analysts and engineers.", + "group": "primary", + "is_array": true, + "requirement": "required", + "caption": "OSINT", + "object_name": "OSINT", + "object_type": "osint", + "_source": "base_event" + } + }, + { + "auth_type": { + "type": "string_t", + "description": "The SSH authentication type, normalized to the caption of 'auth_type_id'. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Authentication Type", + "type_name": "String", + "_source": "ssh_activity", + "_sibling_of": "auth_type_id" + } + }, + { + "confidence": { + "profile": "security_control", + "type": "string_t", + "description": "The confidence, normalized to the caption of the confidence_id value. In the case of 'Other', it is defined by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "confidence_id" + } + }, + { + "observation_point_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Neither the source nor destination network endpoint is the observation point.", + "caption": "Neither" + }, + "0": { + "description": "The observation point is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The source network endpoint is the observation point.", + "caption": "Source" + }, + "2": { + "description": "The destination network endpoint is the observation point.", + "caption": "Destination" + }, + "99": { + "description": "The observation point is not mapped. See the observation_point attribute for a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Both the source and destination network endpoint are the observation point. This typically occurs in localhost or internal communications where the source and destination are the same endpoint, often resulting in a connection_info.direction of Local.", + "caption": "Both" + } + }, + "description": "The normalized identifier of the observation point. The observation point identifier indicates whether the source network endpoint, destination network endpoint, or neither served as the observation point for the activity.", + "requirement": "optional", + "caption": "Observation Point ID", + "type_name": "Integer", + "sibling": "observation_point", + "_source": "network" + } + }, + { + "policy": { + "profile": "security_control", + "type": "object_t", + "description": "The policy that pertains to the control that triggered the event, if applicable. For example the name of an anti-malware policy or an access control policy.", + "group": "primary", + "requirement": "optional", + "caption": "Policy", + "object_name": "Policy", + "object_type": "policy", + "_source": "base_event" + } + }, + { + "connection_info": { + "type": "object_t", + "description": "The network connection information.", + "group": "primary", + "requirement": "recommended", + "caption": "Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "network" + } + }, + { + "action_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "The activity was observed, but neither explicitly allowed nor denied. This is common with IDS and EDR controls that report additional information on observed behavior such as TTPs. The disposition_id attribute should be set to a value that conforms to this action, for example 'Logged', 'Alert', 'Detected', 'Count', etc.", + "caption": "Observed" + }, + "0": { + "description": "The action was unknown. The disposition_id attribute may still be set to a non-unknown value, for example 'Custom Action', 'Challenge'.", + "caption": "Unknown" + }, + "1": { + "description": "The activity was allowed. The disposition_id attribute should be set to a value that conforms to this action, for example 'Allowed', 'Approved', 'Delayed', 'No Action', 'Count' etc.", + "caption": "Allowed" + }, + "2": { + "description": "The attempted activity was denied. The disposition_id attribute should be set to a value that conforms to this action, for example 'Blocked', 'Rejected', 'Quarantined', 'Isolated', 'Dropped', 'Access Revoked, etc.", + "caption": "Denied" + }, + "99": { + "description": "The action is not mapped. See the action attribute which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The activity was modified, adjusted, or corrected. The disposition_id attribute should be set appropriately, for example 'Restored', 'Corrected', 'Delayed', 'Captcha', 'Tagged'.", + "caption": "Modified" + } + }, + "description": "The action taken by a control or other policy-based system leading to an outcome or disposition. An unknown action may still correspond to a known disposition. Refer to disposition_id for the outcome of the action.", + "group": "primary", + "requirement": "recommended", + "caption": "Action ID", + "type_name": "Integer", + "sibling": "action", + "_source": "base_event" + } + }, + { + "authorizations": { + "profile": "security_control", + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "base_event" + } + }, + { + "firewall_rule": { + "profile": "security_control", + "type": "object_t", + "description": "The firewall rule that pertains to the control that triggered the event, if applicable.", + "group": "primary", + "requirement": "optional", + "caption": "Firewall Rule", + "object_name": "Firewall Rule", + "object_type": "firewall_rule", + "_source": "base_event" + } + }, + { + "ja4_fingerprint_list": { + "type": "object_t", + "description": "A list of the JA4+ network fingerprints.", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "JA4+ Fingerprints", + "object_name": "JA4+ Fingerprint", + "object_type": "ja4_fingerprint", + "_source": "network" + } + }, + { + "raw_data_hash": { + "type": "object_t", + "description": "The hash, which describes the content of the raw_data field.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "base_event" + } + }, + { + "time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "optional", + "caption": "Event Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "risk_level_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "group": "context", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "base_event", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "risk_details": { + "profile": "security_control", + "type": "string_t", + "description": "Describes the risk associated with the finding.", + "group": "context", + "requirement": "optional", + "caption": "Risk Details", + "type_name": "String", + "_source": "base_event" + } + }, + { + "disposition_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "description": "A suspicious file or other content was moved to a benign location.", + "caption": "Quarantined" + }, + "6": { + "description": "The request was detected as a threat and resulted in the connection being dropped.", + "caption": "Dropped" + }, + "0": { + "description": "The disposition is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Granted access or allowed the action to the protected resource.", + "caption": "Allowed" + }, + "2": { + "description": "Denied access or blocked the action to the protected resource.", + "caption": "Blocked" + }, + "99": { + "description": "The disposition is not mapped. See the disposition attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A session was isolated on the network or within a browser.", + "caption": "Isolated" + }, + "5": { + "description": "A file or other content was deleted.", + "caption": "Deleted" + }, + "7": { + "description": "A custom action was executed such as running of a command script. Use the message attribute of the base class for details.", + "caption": "Custom Action" + }, + "8": { + "description": "A request or submission was approved. For example, when a form was properly filled out and submitted. This is distinct from 1 'Allowed'.", + "caption": "Approved" + }, + "9": { + "description": "A quarantined file or other content was restored to its original location.", + "caption": "Restored" + }, + "10": { + "description": "A suspicious or risky entity was deemed to no longer be suspicious (re-scored).", + "caption": "Exonerated" + }, + "11": { + "description": "A corrupt file or configuration was corrected.", + "caption": "Corrected" + }, + "12": { + "description": "A corrupt file or configuration was partially corrected.", + "caption": "Partially Corrected" + }, + "14": { + "description": "An operation was delayed, for example if a restart was required to finish the operation.", + "caption": "Delayed" + }, + "15": { + "description": "Suspicious activity or a policy violation was detected without further action.", + "caption": "Detected" + }, + "16": { + "description": "The outcome of an operation had no action taken.", + "caption": "No Action" + }, + "17": { + "description": "The operation or action was logged without further action.", + "caption": "Logged" + }, + "18": { + "description": "A file or other entity was marked with extended attributes.", + "caption": "Tagged" + }, + "20": { + "description": "Counted the request or activity but did not determine whether to allow it or block it.", + "caption": "Count" + }, + "21": { + "description": "The request was detected as a threat and resulted in the connection being reset.", + "caption": "Reset" + }, + "22": { + "description": "Required the end user to solve a CAPTCHA puzzle to prove that a human being is sending the request.", + "caption": "Captcha" + }, + "23": { + "description": "Ran a silent challenge that required the client session to verify that it's a browser, and not a bot.", + "caption": "Challenge" + }, + "24": { + "description": "The requestor's access has been revoked due to security policy enforcements. Note: use the Host profile if the User or Actor requestor is not present in the event class.", + "caption": "Access Revoked" + }, + "25": { + "description": "A request or submission was rejected. For example, when a form was improperly filled out and submitted. This is distinct from 2 'Blocked'.", + "caption": "Rejected" + }, + "26": { + "description": "An attempt to access a resource was denied due to an authorization check that failed. This is a more specific disposition than 2 'Blocked' and can be complemented with the authorizations attribute for more detail.", + "caption": "Unauthorized" + }, + "27": { + "description": "An error occurred during the processing of the activity or request. Use the message attribute of the base class for details.", + "caption": "Error" + }, + "13": { + "description": "A corrupt file or configuration was not corrected.", + "caption": "Uncorrected" + }, + "19": { + "description": "The request or activity was detected as a threat and resulted in a notification but request was not blocked.", + "caption": "Alert" + } + }, + "description": "Describes the outcome or action taken by a security control, such as access control checks, malware detections or various types of policy violations.", + "group": "primary", + "requirement": "recommended", + "caption": "Disposition ID", + "type_name": "Integer", + "sibling": "disposition", + "_source": "base_event" + } + }, + { + "type_name": { + "type": "string_t", + "description": "The event/finding type name, as defined by the type_uid.", + "group": "classification", + "requirement": "optional", + "caption": "Type Name", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "type_uid" + } + }, + { + "dst_endpoint": { + "type": "object_t", + "description": "The responder (server) in a network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Destination Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "network" + } + }, + { + "end_time": { + "type": "timestamp_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "count": { + "type": "integer_t", + "description": "The number of times that events in the same logical group occurred during the event Start Time to End Time period.", + "group": "occurrence", + "requirement": "optional", + "caption": "Count", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "category_name": { + "type": "string_t", + "description": "The event category name, as defined by category_uid value: Network Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "category_uid" + } + }, + { + "unmapped": { + "type": "object_t", + "description": "The attributes that are not mapped to the event schema. The names and values of those attributes are specific to the event source.", + "group": "context", + "requirement": "optional", + "caption": "Unmapped Data", + "object_name": "Object", + "object_type": "object", + "_source": "base_event" + } + }, + { + "is_alert": { + "profile": "security_control", + "type": "boolean_t", + "description": "Indicates that the event is considered to be an alertable signal. Should be set to true if disposition_id = Alert among other dispositions, and/or risk_level_id or severity_id of the event is elevated. Not all control events will be alertable, for example if disposition_id = Exonerated or disposition_id = Allowed.", + "group": "primary", + "requirement": "recommended", + "caption": "Alert", + "type_name": "Boolean", + "_source": "base_event" + } + }, + { + "client_hassh": { + "type": "object_t", + "description": "The Client HASSH fingerprinting object.", + "group": "primary", + "requirement": "recommended", + "caption": "Client HASSH", + "object_name": "HASSH", + "object_type": "hassh", + "_source": "ssh_activity" + } + }, + { + "type_uid": { + "type": "long_t", + "enum": { + "400703": { + "description": "The network connection was abnormally terminated or closed by a middle device like firewalls.", + "caption": "SSH Activity: Reset" + }, + "400706": { + "description": "Network traffic report.", + "caption": "SSH Activity: Traffic" + }, + "400700": { + "caption": "SSH Activity: Unknown" + }, + "400701": { + "description": "A new network connection was opened.", + "caption": "SSH Activity: Open" + }, + "400702": { + "description": "The network connection was closed.", + "caption": "SSH Activity: Close" + }, + "400799": { + "caption": "SSH Activity: Other" + }, + "400704": { + "description": "The network connection failed. For example a connection timeout or no route to host.", + "caption": "SSH Activity: Fail" + }, + "400705": { + "description": "The network connection was refused. For example an attempt to connect to a server port which is not open.", + "caption": "SSH Activity: Refuse" + }, + "400707": { + "description": "A network endpoint began listening for new network connections.", + "caption": "SSH Activity: Listen" + } + }, + "description": "The event/finding type ID. It identifies the event's semantics and structure. The value is calculated by the logging system as: class_uid * 100 + activity_id.", + "group": "classification", + "requirement": "required", + "caption": "Type ID", + "type_name": "Long", + "sibling": "type_name", + "_source": "ssh_activity" + } + }, + { + "confidence_id": { + "profile": "security_control", + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "description": "The normalized confidence is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The confidence is not mapped to the defined enum values. See the confidence attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized confidence refers to the accuracy of the rule that created the finding. A rule with a low confidence means that the finding scope is wide and may create finding reports that may not be malicious in nature.", + "group": "context", + "requirement": "recommended", + "caption": "Confidence ID", + "type_name": "Integer", + "sibling": "confidence", + "_source": "base_event" + } + }, + { + "category_uid": { + "type": "integer_t", + "enum": { + "4": { + "description": "Network Activity events.", + "uid": 4, + "caption": "Network Activity" + } + }, + "description": "The category unique identifier of the event.", + "group": "classification", + "requirement": "required", + "caption": "Category ID", + "type_name": "Integer", + "sibling": "category_name", + "_source": "ssh_activity" + } + }, + { + "proxy_traffic": { + "profile": "network_proxy", + "type": "object_t", + "description": "The network traffic refers to the amount of data moving across a network, from proxy to remote server at a given point of time.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "time": { + "type": "timestamp_t", + "description": "The normalized event occurrence time or the finding creation time.", + "group": "occurrence", + "requirement": "required", + "caption": "Event Time", + "type_name": "Timestamp", + "_source": "base_event" + } + }, + { + "status": { + "type": "string_t", + "description": "The event status, normalized to the caption of the status_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "recommended", + "caption": "Status", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "status_id" + } + }, + { + "duration": { + "type": "long_t", + "description": "The event duration or aggregate time, the amount of time the event covers from start_time to end_time in milliseconds.", + "group": "occurrence", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "load_balancer": { + "profile": "load_balancer", + "type": "object_t", + "description": "The Load Balancer object contains information related to the device that is distributing incoming traffic to specified destinations.", + "group": "primary", + "requirement": "recommended", + "caption": "Load Balancer", + "object_name": "Load Balancer", + "object_type": "load_balancer", + "_source": "network" + } + }, + { + "app_name": { + "type": "string_t", + "description": "The name of the application associated with the event or object.", + "group": "context", + "requirement": "optional", + "caption": "Application Name", + "type_name": "String", + "_source": "network" + } + }, + { + "src_endpoint": { + "type": "object_t", + "description": "The initiator (client) of the network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Source Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "network" + } + }, + { + "proxy_tls": { + "profile": "network_proxy", + "type": "object_t", + "description": "The TLS protocol negotiated between the proxy server and the remote server.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "network" + } + }, + { + "malware": { + "profile": "security_control", + "type": "object_t", + "description": "A list of Malware objects, describing details about the identified malware.", + "group": "primary", + "is_array": true, + "requirement": "optional", + "caption": "Malware", + "object_name": "Malware", + "object_type": "malware", + "_source": "base_event" + } + }, + { + "metadata": { + "type": "object_t", + "description": "The metadata associated with the event or a finding.", + "group": "context", + "requirement": "required", + "caption": "Metadata", + "object_name": "Metadata", + "object_type": "metadata", + "_source": "base_event" + } + }, + { + "traffic": { + "type": "object_t", + "description": "The network traffic for this observation period. Use when reporting: (1) delta values (bytes/packets transferred since the last observation), (2) instantaneous measurements at a specific point in time, or (3) standalone single-event metrics. This attribute represents a point-in-time measurement or incremental change, not a running total. For accumulated totals across multiple observations or the lifetime of a flow, use cumulative_traffic instead.", + "group": "primary", + "requirement": "recommended", + "caption": "Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "confidence_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The confidence score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Confidence Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "proxy": { + "type": "object_t", + "description": "The proxy (server) in a network connection.", + "group": "primary", + "requirement": "recommended", + "caption": "Proxy", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "@deprecated": { + "message": "Use the proxy_endpoint attribute instead.", + "since": "1.1.0" + }, + "_source": "network" + } + }, + { + "enrichments": { + "type": "object_t", + "description": "The additional information from an external data source, which is associated with the event or a finding. For example add location information for the IP address in the DNS answers:

[{\"name\": \"answers.ip\", \"value\": \"92.24.47.250\", \"type\": \"location\", \"data\": {\"city\": \"Socotra\", \"continent\": \"Asia\", \"coordinates\": [-25.4153, 17.0743], \"country\": \"YE\", \"desc\": \"Yemen\"}}]", + "group": "context", + "is_array": true, + "requirement": "optional", + "caption": "Enrichments", + "object_name": "Enrichment", + "object_type": "enrichment", + "_source": "base_event" + } + }, + { + "file": { + "type": "object_t", + "description": "The file that is the target of the SSH activity.", + "group": "context", + "requirement": "optional", + "caption": "File", + "object_name": "File", + "object_type": "file", + "_source": "ssh_activity" + } + }, + { + "status_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "The status is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Success" + }, + "2": { + "caption": "Failure" + }, + "99": { + "description": "The status is not mapped. See the status attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the event status.", + "group": "primary", + "requirement": "recommended", + "caption": "Status ID", + "type_name": "Integer", + "sibling": "status", + "_source": "base_event" + } + }, + { + "class_name": { + "type": "string_t", + "description": "The event class name, as defined by class_uid value: SSH Activity.", + "group": "classification", + "requirement": "optional", + "caption": "Class", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "class_uid" + } + }, + { + "status_detail": { + "type": "string_t", + "description": "The status detail contains additional information about the event/finding outcome.", + "group": "primary", + "requirement": "recommended", + "caption": "Status Detail", + "type_name": "String", + "_source": "base_event" + } + }, + { + "proxy_connection_info": { + "profile": "network_proxy", + "type": "object_t", + "description": "The connection information from the proxy server to the remote server.", + "group": "context", + "requirement": "recommended", + "caption": "Proxy Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "network" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "group": "primary", + "requirement": "recommended", + "caption": "Message", + "type_name": "String", + "_source": "base_event" + } + }, + { + "end_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The end time of a time period, or the time of the most recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "End Time", + "type_name": "Datetime", + "_source": "base_event" + } + }, + { + "api": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about a typical API (Application Programming Interface) call.", + "group": "context", + "requirement": "optional", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "base_event" + } + }, + { + "device": { + "profile": "host", + "type": "object_t", + "description": "An addressable device, computer system or host.", + "group": "primary", + "requirement": "recommended", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "base_event" + } + }, + { + "action": { + "profile": "security_control", + "type": "string_t", + "description": "The normalized caption of action_id.", + "group": "primary", + "requirement": "optional", + "caption": "Action", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "action_id" + } + }, + { + "severity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Action is required but the situation is not serious at this time.", + "caption": "Medium" + }, + "6": { + "description": "An error occurred but it is too late to take remedial action.", + "caption": "Fatal" + }, + "0": { + "description": "The event/finding severity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Informational message. No action required.", + "caption": "Informational" + }, + "2": { + "description": "The user decides if action is needed.", + "caption": "Low" + }, + "99": { + "description": "The event/finding severity is not mapped. See the severity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Action is required immediately.", + "caption": "High" + }, + "5": { + "description": "Action is required immediately and the scope is broad.", + "caption": "Critical" + } + }, + "description": "

The normalized identifier of the event/finding severity.

The normalized severity is a measurement the effort and expense required to manage and resolve an event or incident. Smaller numerical values represent lower impact events, and larger numerical values represent higher impact events.", + "group": "classification", + "requirement": "required", + "caption": "Severity ID", + "type_name": "Integer", + "sibling": "severity", + "_source": "base_event" + } + }, + { + "attacks": { + "profile": "security_control", + "type": "object_t", + "description": "An array of MITRE ATT&CK\u00ae objects describing identified tactics, techniques & sub-techniques. The objects are compatible with MITRE ATLAS\u2122 tactics, techniques & sub-techniques.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "base_event" + } + }, + { + "timezone_offset": { + "type": "integer_t", + "description": "The number of minutes that the reported event time is ahead or behind UTC, in the range -1,080 to +1,080.", + "group": "occurrence", + "requirement": "recommended", + "caption": "Timezone Offset", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "activity_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "The network connection was abnormally terminated or closed by a middle device like firewalls.", + "caption": "Reset" + }, + "6": { + "description": "Network traffic report.", + "caption": "Traffic" + }, + "0": { + "description": "The event activity is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A new network connection was opened.", + "caption": "Open" + }, + "2": { + "description": "The network connection was closed.", + "caption": "Close" + }, + "99": { + "description": "The event activity is not mapped. See the activity_name attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The network connection failed. For example a connection timeout or no route to host.", + "caption": "Fail" + }, + "5": { + "description": "The network connection was refused. For example an attempt to connect to a server port which is not open.", + "caption": "Refuse" + }, + "7": { + "description": "A network endpoint began listening for new network connections.", + "caption": "Listen" + } + }, + "description": "The normalized identifier of the activity that triggered the event.", + "group": "classification", + "requirement": "required", + "caption": "Activity ID", + "type_name": "Integer", + "sibling": "activity_name", + "_source": "ssh_activity", + "suppress_checks": [ + "sibling_convention" + ] + } + }, + { + "malware_scan_info": { + "profile": "security_control", + "type": "object_t", + "description": "Describes details about the scan job that identified malware on the target system.", + "group": "primary", + "requirement": "optional", + "caption": "Malware Scan Info", + "object_name": "Malware Scan Info", + "object_type": "malware_scan_info", + "_source": "base_event" + } + }, + { + "class_uid": { + "type": "integer_t", + "enum": { + "4007": { + "description": "SSH Activity events report remote client connections to a server using the Secure Shell (SSH) Protocol.", + "caption": "SSH Activity" + } + }, + "description": "The unique identifier of a class. A class describes the attributes available in an event.", + "group": "classification", + "requirement": "required", + "caption": "Class ID", + "type_name": "Integer", + "sibling": "class_name", + "_source": "ssh_activity" + } + }, + { + "risk_score": { + "profile": "security_control", + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "group": "context", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "base_event" + } + }, + { + "protocol_ver": { + "type": "string_t", + "description": "The Secure Shell Protocol version.", + "group": "context", + "requirement": "recommended", + "caption": "SSH Version", + "type_name": "String", + "_source": "ssh_activity" + } + }, + { + "raw_data_size": { + "type": "long_t", + "description": "The size of the raw data which was transformed into an OCSF event, in bytes.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data Size", + "type_name": "Long", + "_source": "base_event" + } + }, + { + "observables": { + "type": "object_t", + "description": "The observables associated with the event or a finding.", + "group": "primary", + "is_array": true, + "references": [ + { + "description": "OCSF Observables FAQ", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/defining-and-using-observables.md" + } + ], + "requirement": "recommended", + "caption": "Observables", + "object_name": "Observable", + "object_type": "observable", + "_source": "base_event" + } + }, + { + "auth_type_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "Authentication based on the client host's identity.", + "caption": "Host Based" + }, + "6": { + "description": "Paired public key authentication.", + "caption": "Public Key" + }, + "0": { + "description": "The authentication type is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "Authentication using digital certificates.", + "caption": "Certificate Based" + }, + "2": { + "description": "GSSAPI for centralized authentication.", + "caption": "GSSAPI" + }, + "99": { + "description": "The authentication type is not mapped. See the auth_type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "Multi-step, interactive authentication.", + "caption": "Keyboard Interactive" + }, + "5": { + "description": "Password Authentication.", + "caption": "Password" + } + }, + "description": "The normalized identifier of the SSH authentication type.", + "group": "primary", + "requirement": "recommended", + "caption": "Authentication Type ID", + "type_name": "Integer", + "sibling": "auth_type", + "_source": "ssh_activity" + } + }, + { + "disposition": { + "profile": "security_control", + "type": "string_t", + "description": "The disposition name, normalized to the caption of the disposition_id value. In the case of 'Other', it is defined by the event source.", + "group": "primary", + "requirement": "optional", + "caption": "Disposition", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "disposition_id" + } + }, + { + "proxy_http_response": { + "profile": "network_proxy", + "type": "object_t", + "description": "The HTTP Response from the remote server to the proxy server.", + "group": "context", + "requirement": "optional", + "caption": "Proxy HTTP Response", + "object_name": "HTTP Response", + "object_type": "http_response", + "_source": "network" + } + }, + { + "activity_name": { + "type": "string_t", + "description": "The event activity name, as defined by the activity_id.", + "group": "classification", + "requirement": "optional", + "caption": "Activity", + "type_name": "String", + "_source": "base_event", + "_sibling_of": "activity_id" + } + }, + { + "cloud": { + "profile": "cloud", + "type": "object_t", + "description": "Describes details about the Cloud environment where the event or finding was created.", + "group": "primary", + "requirement": "required", + "caption": "Cloud", + "object_name": "Cloud", + "object_type": "cloud", + "_source": "base_event" + } + }, + { + "actor": { + "profile": "host", + "type": "object_t", + "description": "The actor object describes details about the user/role/process that was the source of the activity. Note that this is not the threat actor of a campaign but may be part of a campaign.", + "group": "primary", + "requirement": "optional", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "base_event" + } + }, + { + "tls": { + "type": "object_t", + "description": "The Transport Layer Security (TLS) attributes.", + "group": "context", + "requirement": "optional", + "caption": "TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "network" + } + }, + { + "raw_data": { + "type": "string_t", + "description": "The raw event/finding data as received from the source.", + "group": "context", + "requirement": "optional", + "caption": "Raw Data", + "type_name": "String", + "_source": "base_event" + } + }, + { + "cumulative_traffic": { + "type": "object_t", + "description": "The cumulative (running total) network traffic aggregated from the start of a flow or session. Use when reporting: (1) total accumulated bytes/packets since flow initiation, (2) combined aggregation models where both incremental deltas and running totals are reported together (populate both traffic for the delta and this attribute for the cumulative total), or (3) final summary metrics when a long-lived connection closes. This represents the sum of all activity from flow start to the current observation, not a delta or point-in-time value.", + "group": "context", + "requirement": "optional", + "caption": "Cumulative Traffic", + "object_name": "Network Traffic", + "object_type": "network_traffic", + "_source": "network" + } + }, + { + "start_time": { + "type": "timestamp_t", + "description": "The start time of a time period, or the time of the least recent event included in the aggregate event.", + "group": "occurrence", + "requirement": "optional", + "caption": "Start Time", + "type_name": "Timestamp", + "_source": "base_event" + } + } + ], + "name": "ssh_activity", + "description": "SSH Activity events report remote client connections to a server using the Secure Shell (SSH) Protocol.", + "uid": 4007, + "extends": "network", + "category": "network", + "constraints": { + "at_least_one": [ + "dst_endpoint", + "src_endpoint" + ] + }, + "profiles": [ + "cloud", + "datetime", + "host", + "osint", + "security_control", + "network_proxy", + "load_balancer", + "data_classification", + "container", + "linux/linux_users" + ], + "category_uid": 4, + "caption": "SSH Activity", + "category_name": "Network Activity" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/actor.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/actor.json new file mode 100644 index 00000000..8202259f --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/actor.json @@ -0,0 +1,118 @@ +{ + "attributes": [ + { + "process": { + "type": "object_t", + "description": "The process that initiated the activity.", + "requirement": "recommended", + "caption": "Process", + "object_name": "Process", + "object_type": "process", + "_source": "actor" + } + }, + { + "session": { + "type": "object_t", + "description": "The user session from which the activity was initiated.", + "requirement": "optional", + "caption": "Session", + "object_name": "Session", + "object_type": "session", + "_source": "actor" + } + }, + { + "user": { + "type": "object_t", + "description": "The user that initiated the activity or the user context from which the activity was initiated.", + "requirement": "recommended", + "caption": "User", + "object_name": "User", + "object_type": "user", + "_source": "actor" + } + }, + { + "app_name": { + "type": "string_t", + "description": "The client application or service that initiated the activity. This can be in conjunction with the user if present. Note that app_name is distinct from the process if present.", + "requirement": "optional", + "caption": "Application Name", + "type_name": "String", + "_source": "actor" + } + }, + { + "app_uid": { + "type": "string_t", + "description": "The unique identifier of the client application or service that initiated the activity. This can be in conjunction with the user if present. Note that app_name is distinct from the process.pid or process.uid if present.", + "requirement": "optional", + "caption": "Application ID", + "type_name": "String", + "_source": "actor" + } + }, + { + "authorizations": { + "type": "object_t", + "description": "Provides details about an authorization, such as authorization outcome, and any associated policies related to the activity/event.", + "is_array": true, + "requirement": "optional", + "caption": "Authorization Information", + "object_name": "Authorization Result", + "object_type": "authorization", + "_source": "actor" + } + }, + { + "idp": { + "type": "object_t", + "description": "This object describes details about the Identity Provider used.", + "requirement": "optional", + "caption": "Identity Provider", + "object_name": "Identity Provider", + "object_type": "idp", + "_source": "actor" + } + }, + { + "invoked_by": { + "type": "string_t", + "description": "The name of the service that invoked the activity as described in the event.", + "requirement": "optional", + "caption": "Invoked by", + "type_name": "String", + "@deprecated": { + "message": "Use app_name, app_uid attributes instead.", + "since": "1.2.0" + }, + "_source": "actor" + } + } + ], + "name": "actor", + "description": "The Actor object contains details about the user, role, application, service, or process that initiated or performed a specific activity. Note that Actor is not the threat actor of a campaign but may be part of a campaign.", + "extends": "object", + "constraints": { + "at_least_one": [ + "process", + "user", + "invoked_by", + "session", + "app_name", + "app_uid" + ] + }, + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:Agent.", + "url": "https://next.d3fend.mitre.org/agent/d3f:Agent/" + } + ], + "profiles": [ + "container", + "linux/linux_users" + ], + "caption": "Actor" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/attack.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/attack.json new file mode 100644 index 00000000..ebf95e4f --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/attack.json @@ -0,0 +1,135 @@ +{ + "attributes": [ + { + "version": { + "type": "string_t", + "description": "The ATT&CK\u00ae or ATLAS\u2122 Matrix version.", + "requirement": "recommended", + "caption": "Version", + "type_name": "String", + "_source": "attack" + } + }, + { + "tactics": { + "type": "object_t", + "description": "The Tactic object describes the tactic ID and/or tactic name that are associated with the attack technique, as defined by ATT&CK\u00ae Matrix.", + "is_array": true, + "requirement": "optional", + "caption": "Tactics", + "object_name": "MITRE Tactic", + "object_type": "tactic", + "@deprecated": { + "message": "Use the tactic attribute instead.", + "since": "1.1.0" + }, + "_source": "attack" + } + }, + { + "technique": { + "type": "object_t", + "description": "The Technique object describes the MITRE ATT&CK\u00ae or ATLAS\u2122 Technique ID and/or name associated to an attack.", + "references": [ + { + "description": "ATT&CK\u00ae Matrix", + "url": "https://attack.mitre.org/wiki/ATT&CK_Matrix" + }, + { + "description": "ATLAS\u2122 Matrix", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "recommended", + "caption": "MITRE Technique", + "object_name": "MITRE Technique", + "object_type": "technique", + "_source": "attack" + } + }, + { + "mitigation": { + "type": "object_t", + "description": "The Mitigation object describes the MITRE ATT&CK\u00ae or ATLAS\u2122 Mitigation ID and/or name that is associated to an attack.", + "references": [ + { + "description": "ATT&CK\u00ae Matrix", + "url": "https://attack.mitre.org/wiki/ATT&CK_Matrix" + }, + { + "description": "ATLAS\u2122 Matrix", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE Mitigation", + "object_name": "MITRE Mitigation", + "object_type": "mitigation", + "_source": "attack" + } + }, + { + "sub_technique": { + "type": "object_t", + "description": "The Sub-technique object describes the MITRE ATT&CK\u00ae or ATLAS\u2122 Sub-technique ID and/or name associated to an attack.", + "references": [ + { + "description": "ATT&CK\u00ae Matrix", + "url": "https://attack.mitre.org/wiki/ATT&CK_Matrix" + }, + { + "description": "ATLAS\u2122 Matrix", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "recommended", + "caption": "MITRE Sub-technique", + "object_name": "MITRE Sub-technique", + "object_type": "sub_technique", + "_source": "attack" + } + }, + { + "tactic": { + "type": "object_t", + "description": "The Tactic object describes the MITRE ATT&CK\u00ae or ATLAS\u2122 Tactic ID and/or name that is associated to an attack.", + "references": [ + { + "description": "ATT&CK\u00ae Matrix", + "url": "https://attack.mitre.org/wiki/ATT&CK_Matrix" + }, + { + "description": "ATLAS\u2122 Matrix", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "recommended", + "caption": "MITRE Tactic", + "object_name": "MITRE Tactic", + "object_type": "tactic", + "_source": "attack" + } + } + ], + "name": "attack", + "description": "The MITRE ATT&CK\u00ae & ATLAS\u2122 object describes the tactic, technique, sub-technique & mitigation associated to an attack.", + "extends": "object", + "constraints": { + "at_least_one": [ + "tactic", + "technique", + "sub_technique" + ] + }, + "references": [ + { + "description": "ATT&CK\u00ae Matrix", + "url": "https://attack.mitre.org/wiki/ATT&CK_Matrix" + }, + { + "description": "ATLAS\u2122 Matrix", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "caption": "MITRE ATT&CK\u00ae & ATLAS\u2122" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/connection_info.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/connection_info.json new file mode 100644 index 00000000..7ad5decc --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/connection_info.json @@ -0,0 +1,3 @@ +{ + "error": "Object connection_info not found" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/container.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/container.json new file mode 100644 index 00000000..8c4f3f80 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/container.json @@ -0,0 +1,150 @@ +{ + "attributes": [ + { + "name": { + "type": "string_t", + "description": "The container name.", + "requirement": "recommended", + "caption": "Name", + "type_name": "String", + "_source": "container" + } + }, + { + "runtime": { + "type": "string_t", + "description": "The backend running the container, such as containerd or cri-o.", + "requirement": "optional", + "caption": "Runtime", + "type_name": "String", + "_source": "container" + } + }, + { + "size": { + "type": "long_t", + "description": "The size of the container image.", + "requirement": "recommended", + "caption": "Size", + "type_name": "Long", + "_source": "container" + } + }, + { + "tag": { + "type": "string_t", + "description": "The tag used by the container. It can indicate version, format, OS.", + "requirement": "optional", + "caption": "Image Tag", + "type_name": "String", + "@deprecated": { + "message": "Use the labels or tags attribute instead.", + "since": "1.4.0" + }, + "_source": "container" + } + }, + { + "uid": { + "type": "string_t", + "description": "The full container unique identifier for this instantiation of the container. For example: ac2ea168264a08f9aaca0dfc82ff3551418dfd22d02b713142a6843caa2f61bf.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "_source": "container" + } + }, + { + "image": { + "type": "object_t", + "description": "The container image used as a template to run the container.", + "requirement": "recommended", + "caption": "Image", + "object_name": "Image", + "object_type": "image", + "_source": "container" + } + }, + { + "hash": { + "type": "object_t", + "description": "Commit hash of image created for docker or the SHA256 hash of the container. For example: 13550340a8681c84c861aac2e5b440161c2b33a3e4f302ac680ca5b686de48de.", + "requirement": "recommended", + "caption": "Hash", + "object_name": "Fingerprint", + "object_type": "fingerprint", + "_source": "container" + } + }, + { + "labels": { + "type": "string_t", + "description": "The list of labels associated to the container.", + "is_array": true, + "requirement": "optional", + "caption": "Labels", + "type_name": "String", + "_source": "container" + } + }, + { + "tags": { + "type": "object_t", + "description": "The list of tags; {key:value} pairs associated to the container.", + "is_array": true, + "requirement": "optional", + "caption": "Tags", + "object_name": "Key:Value object", + "object_type": "key_value_object", + "_source": "container" + } + }, + { + "network_driver": { + "type": "string_t", + "description": "The network driver used by the container. For example, bridge, overlay, host, none, etc.", + "requirement": "optional", + "caption": "Network Driver", + "type_name": "String", + "_source": "container" + } + }, + { + "orchestrator": { + "type": "string_t", + "description": "The orchestrator managing the container, such as ECS, EKS, K8s, or OpenShift.", + "requirement": "optional", + "caption": "Orchestrator", + "type_name": "String", + "_source": "container" + } + }, + { + "pod_uuid": { + "type": "uuid_t", + "description": "The unique identifier of the pod (or equivalent) that the container is executing on.", + "requirement": "optional", + "caption": "Pod UUID", + "type_name": "UUID", + "_source": "container" + } + } + ], + "name": "container", + "description": "The Container object describes an instance of a specific container. A container is a prepackaged, portable system image that runs isolated on an existing system using a container runtime like containerd.", + "extends": "object", + "constraints": { + "at_least_one": [ + "uid", + "name" + ] + }, + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:ContainerProcess.", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:ContainerProcess/" + } + ], + "caption": "Container", + "observable": 27 +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/device.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/device.json new file mode 100644 index 00000000..94a37a9d --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/device.json @@ -0,0 +1,798 @@ +{ + "attributes": [ + { + "boot_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time the system was booted.", + "requirement": "optional", + "caption": "Boot Time", + "type_name": "Datetime", + "_source": "device" + } + }, + { + "risk_level": { + "type": "string_t", + "description": "The risk level, normalized to the caption of the risk_level_id value.", + "requirement": "optional", + "caption": "Risk Level", + "type_name": "String", + "_source": "device", + "_sibling_of": "risk_level_id" + } + }, + { + "vendor_name": { + "type": "string_t", + "description": "The vendor for the device. For example Dell or Lenovo.", + "requirement": "recommended", + "caption": "Vendor Name", + "type_name": "String", + "_source": "device" + } + }, + { + "type_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "A laptop computer.", + "caption": "Laptop" + }, + "6": { + "description": "A virtual machine.", + "caption": "Virtual" + }, + "0": { + "description": "The type is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A server.", + "caption": "Server" + }, + "2": { + "description": "A desktop computer.", + "caption": "Desktop" + }, + "99": { + "description": "The type is not mapped. See the type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A tablet computer.", + "caption": "Tablet" + }, + "5": { + "description": "A mobile phone.", + "caption": "Mobile" + }, + "7": { + "description": "An IOT (Internet of Things) device.", + "caption": "IOT" + }, + "8": { + "description": "A web browser.", + "caption": "Browser" + }, + "9": { + "description": "A networking firewall.", + "caption": "Firewall" + }, + "10": { + "description": "A networking switch.", + "caption": "Switch" + }, + "11": { + "description": "A networking hub.", + "caption": "Hub" + }, + "12": { + "description": "A networking router.", + "caption": "Router" + }, + "14": { + "description": "An intrusion prevention system.", + "caption": "IPS" + }, + "15": { + "description": "A Load Balancer device.", + "caption": "Load Balancer" + }, + "13": { + "description": "An intrusion detection system.", + "caption": "IDS" + } + }, + "description": "The device type ID.", + "requirement": "required", + "caption": "Type ID", + "type_name": "Integer", + "sibling": "type", + "_source": "device" + } + }, + { + "imei_list": { + "type": "string_t", + "description": "The International Mobile Equipment Identity values that are associated with the device.", + "is_array": true, + "requirement": "optional", + "caption": "IMEI List", + "type_name": "String", + "_source": "device" + } + }, + { + "first_seen_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The initial discovery time of the device.", + "requirement": "optional", + "caption": "First Seen", + "type_name": "Datetime", + "_source": "device" + } + }, + { + "org": { + "type": "object_t", + "description": "Organization and org unit related to the device.", + "requirement": "optional", + "caption": "Organization", + "object_name": "Organization", + "object_type": "organization", + "_source": "device" + } + }, + { + "type": { + "type": "string_t", + "description": "The device type. For example: unknown, server, desktop, laptop, tablet, mobile, virtual, browser, or other.", + "requirement": "recommended", + "caption": "Type", + "type_name": "String", + "_source": "device", + "_sibling_of": "type_id" + } + }, + { + "interface_uid": { + "type": "string_t", + "description": "The unique identifier of the network interface.", + "requirement": "recommended", + "caption": "Network Interface ID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "is_trusted": { + "type": "boolean_t", + "description": "The event occurred on a trusted device.", + "requirement": "optional", + "caption": "Trusted Device", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "is_shared": { + "type": "boolean_t", + "description": "The event occurred on a shared device.", + "requirement": "optional", + "caption": "Shared Device", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "boot_uid": { + "type": "string_t", + "description": "A unique identifier of the device that changes after every reboot. For example, the value of /proc/sys/kernel/random/boot_id from Linux's procfs.", + "references": [ + { + "description": "Linux kernel's documentation", + "url": "https://docs.kernel.org/admin-guide/sysctl/kernel.html#random" + } + ], + "requirement": "optional", + "caption": "Boot UID", + "type_name": "String", + "_source": "device" + } + }, + { + "subnet": { + "type": "subnet_t", + "description": "The subnet mask.", + "requirement": "optional", + "caption": "Subnet", + "type_name": "Subnet", + "_source": "device" + } + }, + { + "name": { + "type": "string_t", + "description": "The alternate device name, ordinarily as assigned by an administrator.

Note: The Name could be any other string that helps to identify the device, such as a phone number; for example 310-555-1234.

", + "requirement": "optional", + "caption": "Name", + "type_name": "String", + "_source": "device" + } + }, + { + "region": { + "type": "string_t", + "description": "The region where the virtual machine is located. For example, an AWS Region.", + "requirement": "recommended", + "caption": "Region", + "type_name": "String", + "_source": "device" + } + }, + { + "udid": { + "type": "string_t", + "description": "The Apple assigned Unique Device Identifier (UDID). For iOS, iPadOS, tvOS, watchOS and visionOS devices, this is the UDID. For macOS devices, it is the Provisioning UDID. For example: 00008020-008D4548007B4F26", + "references": [ + { + "description": "Apple Wiki", + "url": "https://theapplewiki.com/wiki/UDID" + } + ], + "requirement": "optional", + "caption": "Unique Device Identifier", + "type_name": "String", + "_source": "device" + } + }, + { + "last_seen_time": { + "type": "timestamp_t", + "description": "The most recent discovery time of the device.", + "requirement": "optional", + "caption": "Last Seen", + "type_name": "Timestamp", + "_source": "device" + } + }, + { + "modified_time": { + "type": "timestamp_t", + "description": "The time when the device was last known to have been modified.", + "requirement": "optional", + "caption": "Modified Time", + "type_name": "Timestamp", + "_source": "device" + } + }, + { + "risk_level_id": { + "type": "integer_t", + "enum": { + "3": { + "caption": "High" + }, + "0": { + "caption": "Info" + }, + "1": { + "caption": "Low" + }, + "2": { + "caption": "Medium" + }, + "99": { + "description": "The risk level is not mapped. See the risk_level attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "Critical" + } + }, + "description": "The normalized risk level id.", + "requirement": "optional", + "caption": "Risk Level ID", + "type_name": "Integer", + "sibling": "risk_level", + "_source": "device", + "suppress_checks": [ + "enum_convention" + ] + } + }, + { + "created_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the device was known to have been created.", + "requirement": "optional", + "caption": "Created Time", + "type_name": "Datetime", + "_source": "device" + } + }, + { + "is_mobile_account_active": { + "type": "boolean_t", + "description": "Indicates whether the device has an active mobile account. For example, this is indicated by the itunesStoreAccountActive value within JAMF Pro mobile devices.", + "requirement": "optional", + "caption": "Mobile Account Active", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "hostname": { + "type": "hostname_t", + "description": "The device hostname.", + "requirement": "recommended", + "caption": "Hostname", + "type_name": "Hostname", + "_source": "device" + } + }, + { + "modified_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the device was last known to have been modified.", + "requirement": "optional", + "caption": "Modified Time", + "type_name": "Datetime", + "_source": "device" + } + }, + { + "mac": { + "type": "mac_t", + "description": "The Media Access Control (MAC) address of the endpoint.", + "requirement": "optional", + "caption": "MAC Address", + "type_name": "MAC Address", + "_source": "endpoint" + } + }, + { + "instance_uid": { + "type": "string_t", + "description": "The unique identifier of a VM instance.", + "requirement": "recommended", + "caption": "Instance ID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "domain": { + "type": "string_t", + "description": "The network domain where the device resides. For example: work.example.com.", + "requirement": "optional", + "caption": "Domain", + "type_name": "String", + "_source": "device" + } + }, + { + "hw_info": { + "type": "object_t", + "description": "The endpoint hardware information.", + "requirement": "optional", + "caption": "Hardware Info", + "object_name": "Device Hardware Info", + "object_type": "device_hw_info", + "_source": "endpoint" + } + }, + { + "agent_list": { + "type": "object_t", + "description": "A list of agent objects associated with a device, endpoint, or resource.", + "is_array": true, + "requirement": "optional", + "caption": "Agent List", + "object_name": "Agent", + "object_type": "agent", + "_source": "endpoint" + } + }, + { + "autoscale_uid": { + "type": "string_t", + "description": "The unique identifier of the cloud autoscale configuration.", + "requirement": "optional", + "caption": "Autoscale UID", + "type_name": "String", + "_source": "device" + } + }, + { + "imei": { + "type": "string_t", + "description": "The International Mobile Equipment Identity that is associated with the device.", + "requirement": "optional", + "caption": "IMEI", + "type_name": "String", + "@deprecated": { + "message": "Use the imei_list attribute instead.", + "since": "1.4.0" + }, + "_source": "device" + } + }, + { + "uid_alt": { + "type": "string_t", + "description": "An alternate unique identifier of the device if any. For example the ActiveDirectory DN.", + "requirement": "optional", + "caption": "Alternate ID", + "type_name": "String", + "_source": "device" + } + }, + { + "first_seen_time": { + "type": "timestamp_t", + "description": "The initial discovery time of the device.", + "requirement": "optional", + "caption": "First Seen", + "type_name": "Timestamp", + "_source": "device" + } + }, + { + "is_supervised": { + "type": "boolean_t", + "description": "The event occurred on a supervised device. Devices that are supervised are typically mobile devices managed by a Mobile Device Management solution and are restricted from specific behaviors such as Apple AirDrop.", + "requirement": "optional", + "caption": "Supervised Device", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "ip": { + "type": "ip_t", + "description": "The device IP address, in either IPv4 or IPv6 format.", + "requirement": "optional", + "caption": "IP Address", + "type_name": "IP Address", + "_source": "device" + } + }, + { + "image": { + "type": "object_t", + "description": "The image used as a template to run the virtual machine.", + "requirement": "optional", + "caption": "Image", + "object_name": "Image", + "object_type": "image", + "_source": "device" + } + }, + { + "namespace_pid": { + "profile": "container", + "type": "integer_t", + "description": "If running under a process namespace (such as in a container), the process identifier within that process namespace.", + "group": "context", + "requirement": "recommended", + "caption": "Namespace PID", + "type_name": "Integer", + "_source": "endpoint" + } + }, + { + "is_personal": { + "type": "boolean_t", + "description": "The event occurred on a personal device.", + "requirement": "optional", + "caption": "Personal Device", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "is_compliant": { + "type": "boolean_t", + "description": "The event occurred on a compliant device.", + "requirement": "optional", + "caption": "Compliant Device", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "boot_time": { + "type": "timestamp_t", + "description": "The time the system was booted.", + "requirement": "optional", + "caption": "Boot Time", + "type_name": "Timestamp", + "_source": "device" + } + }, + { + "vpc_uid": { + "type": "string_t", + "description": "The unique identifier of the Virtual Private Cloud (VPC).", + "requirement": "optional", + "caption": "VPC UID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "meid": { + "type": "string_t", + "description": "The Mobile Equipment Identifier. It's a unique number that identifies a Code Division Multiple Access (CDMA) mobile device.", + "requirement": "optional", + "caption": "MEID", + "type_name": "String", + "_source": "device" + } + }, + { + "vlan_uid": { + "type": "string_t", + "description": "The Virtual LAN identifier.", + "requirement": "optional", + "caption": "VLAN", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "os": { + "type": "object_t", + "description": "The endpoint operating system.", + "requirement": "optional", + "caption": "OS", + "object_name": "Operating System (OS)", + "object_type": "os", + "_source": "endpoint" + } + }, + { + "os_machine_uuid": { + "type": "uuid_t", + "description": "The operating system assigned Machine ID. In Windows, this is the value stored at the registry path: HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid. In Linux, this is stored in the file: /etc/machine-id.", + "requirement": "optional", + "caption": "OS Machine UUID", + "type_name": "UUID", + "_source": "device" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the device. For example the Windows TargetSID or AWS EC2 ARN.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "observable": 47, + "_source": "device" + } + }, + { + "owner": { + "type": "object_t", + "description": "The identity of the service or user account that owns the endpoint or was last logged into it.", + "requirement": "recommended", + "caption": "Owner", + "object_name": "User", + "object_type": "user", + "_source": "endpoint" + } + }, + { + "model": { + "type": "string_t", + "description": "The model of the device. For example ThinkPad X1 Carbon.", + "requirement": "optional", + "caption": "Model", + "type_name": "String", + "_source": "device" + } + }, + { + "network_interfaces": { + "type": "object_t", + "description": "The physical or virtual network interfaces that are associated with the device, one for each unique MAC address/IP address/hostname/name combination.

Note: The first element of the array is the network information that pertains to the event.

", + "is_array": true, + "requirement": "optional", + "caption": "Network Interfaces", + "object_name": "Network Interface", + "object_type": "network_interface", + "_source": "device" + } + }, + { + "desc": { + "type": "string_t", + "description": "The description of the device, ordinarily as reported by the operating system.", + "requirement": "optional", + "caption": "Description", + "type_name": "String", + "_source": "device" + } + }, + { + "created_time": { + "type": "timestamp_t", + "description": "The time when the device was known to have been created.", + "requirement": "optional", + "caption": "Created Time", + "type_name": "Timestamp", + "_source": "device" + } + }, + { + "last_seen_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The most recent discovery time of the device.", + "requirement": "optional", + "caption": "Last Seen", + "type_name": "Datetime", + "_source": "device" + } + }, + { + "iccid": { + "type": "string_t", + "description": "The Integrated Circuit Card Identification of a mobile device. Typically it is a unique 18 to 22 digit number that identifies a SIM card.", + "requirement": "optional", + "caption": "ICCID", + "type_name": "String", + "_source": "device" + } + }, + { + "container": { + "profile": "container", + "type": "object_t", + "description": "The information describing an instance of a container. A container is a prepackaged, portable system image that runs isolated on an existing system using a container runtime like containerd.", + "group": "context", + "requirement": "recommended", + "caption": "Container", + "object_name": "Container", + "object_type": "container", + "_source": "endpoint" + } + }, + { + "subnet_uid": { + "type": "string_t", + "description": "The unique identifier of a virtual subnet.", + "requirement": "optional", + "caption": "Subnet UID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "zone": { + "type": "string_t", + "description": "The network zone or LAN segment.", + "requirement": "optional", + "caption": "Network Zone", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "eid": { + "type": "string_t", + "description": "An Embedded Identity Document, is a unique serial number that identifies an eSIM-enabled device.", + "requirement": "optional", + "caption": "EID", + "type_name": "String", + "_source": "device" + } + }, + { + "hypervisor": { + "type": "string_t", + "description": "The name of the hypervisor running on the device. For example, Xen, VMware, Hyper-V, VirtualBox, etc.", + "requirement": "optional", + "caption": "Hypervisor", + "type_name": "String", + "_source": "device" + } + }, + { + "interface_name": { + "type": "string_t", + "description": "The name of the network interface (e.g. eth2).", + "requirement": "recommended", + "caption": "Network Interface Name", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "risk_score": { + "type": "integer_t", + "description": "The risk score as reported by the event source.", + "requirement": "optional", + "caption": "Risk Score", + "type_name": "Integer", + "_source": "device" + } + }, + { + "location": { + "type": "object_t", + "description": "The geographical location of the device.", + "requirement": "optional", + "caption": "Geo Location", + "object_name": "Geo Location", + "object_type": "location", + "_source": "device" + } + }, + { + "groups": { + "type": "object_t", + "description": "The group names to which the device belongs. For example: [\"Windows Laptops\", \"Engineering\"].", + "is_array": true, + "requirement": "optional", + "caption": "Groups", + "object_name": "Group", + "object_type": "group", + "_source": "device" + } + }, + { + "is_managed": { + "type": "boolean_t", + "description": "The event occurred on a managed device.", + "requirement": "optional", + "caption": "Managed Device", + "type_name": "Boolean", + "_source": "device" + } + }, + { + "is_backed_up": { + "type": "boolean_t", + "description": "Indicates whether the device or resource has a backup enabled, such as an automated snapshot or a cloud backup. For example, this is indicated by the cloudBackupEnabled value within JAMF Pro mobile devices or the registration of an AWS ARN with the AWS Backup service.", + "requirement": "optional", + "caption": "Back Ups Configured", + "type_name": "Boolean", + "_source": "device" + } + } + ], + "name": "device", + "description": "The Device object represents an addressable computer system or host, which is typically connected to a computer network and participates in the transmission or processing of data within the computer network.", + "extends": "endpoint", + "constraints": { + "at_least_one": [ + "ip", + "uid", + "name", + "hostname", + "instance_uid", + "interface_uid", + "interface_name" + ] + }, + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:Host.", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:Host/" + } + ], + "profiles": [ + "container", + "datetime" + ], + "caption": "Device", + "observable": 20 +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/evidences.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/evidences.json new file mode 100644 index 00000000..a0a4c950 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/evidences.json @@ -0,0 +1,428 @@ +{ + "attributes": [ + { + "data": { + "type": "json_t", + "description": "Additional evidence data that is not accounted for in the specific evidence attributes. Use only when absolutely necessary.", + "requirement": "optional", + "caption": "Data", + "type_name": "JSON", + "_source": "evidences" + } + }, + { + "http_response": { + "type": "object_t", + "description": "Describes details about the http response associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "HTTP Response", + "object_name": "HTTP Response", + "object_type": "http_response", + "_source": "evidences" + } + }, + { + "http_request": { + "type": "object_t", + "description": "Describes details about the http request associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "HTTP Request", + "object_name": "HTTP Request", + "object_type": "http_request", + "_source": "evidences" + } + }, + { + "name": { + "type": "string_t", + "description": "The naming convention or type identifier of the evidence associated with the security detection. For example, the @odata.type from Microsoft Graph Alerts V2 or display_name from CrowdStrike Falcon Incident Behaviors.", + "requirement": "optional", + "caption": "Name", + "type_name": "String", + "_source": "evidences" + } + }, + { + "process": { + "type": "object_t", + "description": "Describes details about the process associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Process", + "object_name": "Process", + "object_type": "process", + "_source": "evidences" + } + }, + { + "file": { + "type": "object_t", + "description": "Describes details about the file associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "File", + "object_name": "File", + "object_type": "file", + "_source": "evidences" + } + }, + { + "user": { + "type": "object_t", + "description": "Describes details about the user that was the target or somehow else associated with the activity that triggered the detection.", + "requirement": "recommended", + "caption": "User", + "object_name": "User", + "object_type": "user", + "_source": "evidences" + } + }, + { + "script": { + "type": "object_t", + "description": "Describes details about the script that was associated with the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Script", + "object_name": "Script", + "object_type": "script", + "_source": "evidences" + } + }, + { + "device": { + "type": "object_t", + "description": "An addressable device, computer system or host associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Device", + "object_name": "Device", + "object_type": "device", + "_source": "evidences" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the evidence associated with the security detection. For example, the activity_id from CrowdStrike Falcon Alerts or behavior_id from CrowdStrike Falcon Incident Behaviors.", + "requirement": "optional", + "caption": "Unique ID", + "type_name": "String", + "_source": "evidences" + } + }, + { + "query": { + "type": "object_t", + "description": "Describes details about the DNS query associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "DNS Query", + "object_name": "DNS Query", + "object_type": "dns_query", + "_source": "evidences" + } + }, + { + "connection_info": { + "type": "object_t", + "description": "Describes details about the network connection associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Connection Info", + "object_name": "Network Connection Information", + "object_type": "network_connection_info", + "_source": "evidences" + } + }, + { + "url": { + "type": "object_t", + "description": "The URL object that pertains to the event or object associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "URL", + "object_name": "Uniform Resource Locator", + "object_type": "url", + "_source": "evidences" + } + }, + { + "email": { + "type": "object_t", + "description": "The email object associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Email", + "object_name": "Email", + "object_type": "email", + "_source": "evidences" + } + }, + { + "tls": { + "type": "object_t", + "description": "Describes details about the Transport Layer Security (TLS) activity that triggered the detection.", + "requirement": "recommended", + "caption": "TLS", + "object_name": "Transport Layer Security (TLS)", + "object_type": "tls", + "_source": "evidences" + } + }, + { + "api": { + "type": "object_t", + "description": "Describes details about the API call associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "API Details", + "object_name": "API", + "object_type": "api", + "_source": "evidences" + } + }, + { + "resources": { + "type": "object_t", + "description": "Describes details about the cloud resources directly related to activity that triggered the detection. For resources impacted by the detection, use Affected Resources at the top-level of the finding.", + "is_array": true, + "requirement": "recommended", + "caption": "Cloud Resources", + "object_name": "Resource Details", + "object_type": "resource_details", + "_source": "evidences" + } + }, + { + "actor": { + "type": "object_t", + "description": "Describes details about the user/role/process that was the source of the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Actor", + "object_name": "Actor", + "object_type": "actor", + "_source": "evidences" + } + }, + { + "container": { + "type": "object_t", + "description": "Describes details about the container associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Container", + "object_name": "Container", + "object_type": "container", + "_source": "evidences" + } + }, + { + "database": { + "type": "object_t", + "description": "Describes details about the database associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Database", + "object_name": "Database", + "object_type": "database", + "_source": "evidences" + } + }, + { + "databucket": { + "type": "object_t", + "description": "Describes details about the databucket associated to the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Databucket", + "object_name": "Databucket", + "object_type": "databucket", + "_source": "evidences" + } + }, + { + "dst_endpoint": { + "type": "object_t", + "description": "Describes details about the destination of the network activity that triggered the detection.", + "requirement": "recommended", + "caption": "Destination Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "evidences" + } + }, + { + "ja4_fingerprint_list": { + "type": "object_t", + "description": "Describes details about the JA4+ fingerprints that triggered the detection.", + "is_array": true, + "requirement": "recommended", + "caption": "JA4+ Fingerprints", + "object_name": "JA4+ Fingerprint", + "object_type": "ja4_fingerprint", + "_source": "evidences" + } + }, + { + "job": { + "type": "object_t", + "description": "Describes details about the scheduled job that was associated with the activity that triggered the detection.", + "requirement": "recommended", + "caption": "Job", + "object_name": "Job", + "object_type": "job", + "_source": "evidences" + } + }, + { + "src_endpoint": { + "type": "object_t", + "description": "Describes details about the source of the network activity that triggered the detection.", + "requirement": "recommended", + "caption": "Source Endpoint", + "object_name": "Network Endpoint", + "object_type": "network_endpoint", + "_source": "evidences" + } + }, + { + "verdict": { + "type": "string_t", + "description": "The normalized verdict of the evidence associated with the security detection. ", + "requirement": "optional", + "caption": "Verdict", + "type_name": "String", + "_source": "evidences", + "_sibling_of": "verdict_id" + } + }, + { + "verdict_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "The verdict for the evidence is that is should be Disregarded.", + "caption": "Disregard" + }, + "6": { + "description": "The evidence is part of a Test, or other sanctioned behavior(s).", + "caption": "Test" + }, + "0": { + "description": "The type is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "The verdict for the evidence has been identified as a False Positive.", + "caption": "False Positive" + }, + "2": { + "description": "The verdict for the evidence has been identified as a True Positive.", + "caption": "True Positive" + }, + "99": { + "description": "The type is not mapped. See the type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "The verdict for the evidence is that the behavior has been identified as Suspicious.", + "caption": "Suspicious" + }, + "5": { + "description": "The verdict for the evidence is that the behavior has been identified as Benign.", + "caption": "Benign" + }, + "7": { + "description": "There is insufficient data to render a verdict on the evidence.", + "caption": "Insufficient Data" + }, + "8": { + "description": "The verdict for the evidence is that the behavior has been identified as a Security Risk.", + "caption": "Security Risk" + }, + "9": { + "description": "The verdict for the evidence is Managed Externally, such as in a case management tool.", + "caption": "Managed Externally" + }, + "10": { + "description": "This evidence duplicates existing evidence related to this finding.", + "caption": "Duplicate" + } + }, + "description": "The normalized verdict (or status) ID of the evidence associated with the security detection. For example, Microsoft Graph Security Alerts contain a verdict enumeration for each type of evidence associated with the Alert. This is typically set by an automated investigation process or an analyst/investigator assigned to the finding.", + "requirement": "optional", + "caption": "Verdict ID", + "type_name": "Integer", + "sibling": "verdict", + "_source": "evidences" + } + }, + { + "reg_key": { + "type": "object_t", + "description": "Describes details about the registry key that triggered the detection.", + "group": "primary", + "extension": "win", + "requirement": "recommended", + "extension_id": 2, + "caption": "Registry Key", + "object_name": "Registry Key", + "object_type": "win/reg_key", + "_source": "win/evidences", + "_source_patched": "evidences" + } + }, + { + "reg_value": { + "type": "object_t", + "description": "Describes details about the registry value that triggered the detection.", + "group": "primary", + "extension": "win", + "requirement": "recommended", + "extension_id": 2, + "caption": "Registry Value", + "object_name": "Registry Value", + "object_type": "win/reg_value", + "_source": "win/evidences", + "_source_patched": "evidences" + } + }, + { + "win_service": { + "type": "object_t", + "description": "Describes details about the Windows service that triggered the detection.", + "extension": "win", + "requirement": "recommended", + "extension_id": 2, + "caption": "Windows Service", + "object_name": "Windows Service", + "object_type": "win/win_service", + "_source": "win/evidences", + "_source_patched": "evidences" + } + } + ], + "name": "evidences", + "description": "A collection of evidence artifacts associated to the activity/activities that triggered a security detection.", + "extends": "_entity", + "constraints": { + "at_least_one": [ + "actor", + "api", + "connection_info", + "data", + "database", + "databucket", + "device", + "dst_endpoint", + "email", + "file", + "process", + "query", + "src_endpoint", + "url", + "user", + "job", + "script", + "reg_key", + "reg_value", + "win_service" + ] + }, + "profiles": [ + "data_classification", + "cloud", + "container", + "linux/linux_users" + ], + "caption": "Evidence Artifacts" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/finding_info.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/finding_info.json new file mode 100644 index 00000000..03b7dc7b --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/finding_info.json @@ -0,0 +1,318 @@ +{ + "attributes": [ + { + "title": { + "type": "string_t", + "description": "A title or a brief phrase summarizing the reported finding.", + "requirement": "recommended", + "caption": "Title", + "type_name": "String", + "_source": "finding_info" + } + }, + { + "desc": { + "type": "string_t", + "description": "The description of the reported finding.", + "requirement": "optional", + "caption": "Description", + "type_name": "String", + "_source": "finding_info" + } + }, + { + "product": { + "type": "object_t", + "description": "Details about the product that reported the finding.", + "requirement": "optional", + "caption": "Product", + "object_name": "Product", + "object_type": "product", + "_source": "finding_info" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the reported finding.", + "requirement": "required", + "caption": "Unique ID", + "type_name": "String", + "_source": "finding_info" + } + }, + { + "types": { + "type": "string_t", + "description": "One or more types of the reported finding.", + "is_array": true, + "requirement": "optional", + "caption": "Types", + "type_name": "String", + "_source": "finding_info" + } + }, + { + "tags": { + "type": "object_t", + "description": "The list of tags; {key:value} pairs associated with the finding.", + "is_array": true, + "requirement": "optional", + "caption": "Tags", + "object_name": "Key:Value object", + "object_type": "key_value_object", + "_source": "finding_info" + } + }, + { + "attacks": { + "type": "object_t", + "description": "The MITRE ATT&CK\u00ae technique and associated tactics related to the finding.", + "is_array": true, + "references": [ + { + "description": "MITRE ATT&CK\u00ae", + "url": "https://attack.mitre.org" + }, + { + "description": "MITRE ATLAS", + "url": "https://atlas.mitre.org/matrices/ATLAS" + } + ], + "requirement": "optional", + "caption": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "object_name": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "object_type": "attack", + "_source": "finding_info" + } + }, + { + "analytic": { + "type": "object_t", + "description": "The analytic technique used to analyze and derive insights from the data or information that led to the finding or conclusion.", + "requirement": "recommended", + "caption": "Analytic", + "object_name": "Analytic", + "object_type": "analytic", + "_source": "finding_info" + } + }, + { + "attack_graph": { + "type": "object_t", + "description": "An Attack Graph describes possible routes an attacker could take through an environment. It describes relationships between resources and their findings, such as malware detections, vulnerabilities, misconfigurations, and other security actions.", + "group": "context", + "references": [ + { + "description": "MS Defender description of Attack Path", + "url": "https://learn.microsoft.com/en-us/azure/defender-for-cloud/how-to-manage-attack-path" + }, + { + "description": "SentinelOne Attack Path documentation", + "url": "https://www.sentinelone.com/cybersecurity-101/cybersecurity/attack-path-analysis/" + } + ], + "requirement": "optional", + "caption": "Attack Graph", + "object_name": "Graph", + "object_type": "graph", + "_source": "finding_info" + } + }, + { + "created_time": { + "type": "timestamp_t", + "description": "The time when the finding was created.", + "requirement": "optional", + "caption": "Created Time", + "type_name": "Timestamp", + "_source": "finding_info" + } + }, + { + "data_sources": { + "type": "string_t", + "description": "A list of data sources utilized in generation of the finding.", + "is_array": true, + "requirement": "optional", + "caption": "Data Sources", + "type_name": "String", + "_source": "finding_info" + } + }, + { + "first_seen_time": { + "type": "timestamp_t", + "description": "The time when the finding was first observed. e.g. The time when a vulnerability was first observed.

It can differ from the created_time timestamp, which reflects the time this finding was created.

", + "requirement": "optional", + "caption": "First Seen", + "type_name": "Timestamp", + "_source": "finding_info" + } + }, + { + "kill_chain": { + "type": "object_t", + "description": "The Cyber Kill Chain\u00ae provides a detailed description of each phase and its associated activities within the broader context of a cyber attack.", + "is_array": true, + "requirement": "optional", + "caption": "Kill Chain", + "object_name": "Kill Chain Phase", + "object_type": "kill_chain_phase", + "_source": "finding_info" + } + }, + { + "last_seen_time": { + "type": "timestamp_t", + "description": "The time when the finding was most recently observed. e.g. The time when a vulnerability was most recently observed.

It can differ from the modified_time timestamp, which reflects the time this finding was last modified.

", + "requirement": "optional", + "caption": "Last Seen", + "type_name": "Timestamp", + "_source": "finding_info" + } + }, + { + "modified_time": { + "type": "timestamp_t", + "description": "The time when the finding was last modified.", + "requirement": "optional", + "caption": "Modified Time", + "type_name": "Timestamp", + "_source": "finding_info" + } + }, + { + "product_uid": { + "type": "string_t", + "description": "The unique identifier of the product that reported the finding.", + "requirement": "optional", + "caption": "Product Identifier", + "type_name": "String", + "@deprecated": { + "message": "Use the uid attribute in the product object instead. See specific usage.", + "since": "1.4.0" + }, + "_source": "finding_info" + } + }, + { + "related_analytics": { + "type": "object_t", + "description": "Other analytics related to this finding.", + "is_array": true, + "requirement": "optional", + "caption": "Related Analytics", + "object_name": "Analytic", + "object_type": "analytic", + "_source": "finding_info" + } + }, + { + "related_events": { + "type": "object_t", + "description": "Describes events and/or other findings related to the finding as identified by the security product. Note that these events may or may not be in OCSF.", + "is_array": true, + "requirement": "optional", + "caption": "Related Events/Findings", + "object_name": "Related Event/Finding", + "object_type": "related_event", + "_source": "finding_info" + } + }, + { + "related_events_count": { + "type": "integer_t", + "description": "Number of related events or findings.", + "requirement": "optional", + "caption": "Related Events/Findings Count", + "type_name": "Integer", + "_source": "finding_info" + } + }, + { + "src_url": { + "type": "url_t", + "description": "The URL pointing to the source of the finding.", + "requirement": "optional", + "caption": "Source URL", + "type_name": "URL String", + "_source": "finding_info" + } + }, + { + "traits": { + "type": "object_t", + "description": "The list of key traits or characteristics extracted from the finding.", + "is_array": true, + "requirement": "optional", + "caption": "Traits", + "object_name": "Trait", + "object_type": "trait", + "_source": "finding_info" + } + }, + { + "uid_alt": { + "type": "string_t", + "description": "The alternative unique identifier of the reported finding.", + "requirement": "optional", + "caption": "Alternate ID", + "type_name": "String", + "_source": "finding_info" + } + }, + { + "last_seen_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the finding was most recently observed. e.g. The time when a vulnerability was most recently observed.

It can differ from the modified_time timestamp, which reflects the time this finding was last modified.

", + "requirement": "optional", + "caption": "Last Seen", + "type_name": "Datetime", + "_source": "finding_info" + } + }, + { + "modified_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the finding was last modified.", + "requirement": "optional", + "caption": "Modified Time", + "type_name": "Datetime", + "_source": "finding_info" + } + }, + { + "first_seen_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the finding was first observed. e.g. The time when a vulnerability was first observed.

It can differ from the created_time timestamp, which reflects the time this finding was created.

", + "requirement": "optional", + "caption": "First Seen", + "type_name": "Datetime", + "_source": "finding_info" + } + }, + { + "created_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the finding was created.", + "requirement": "optional", + "caption": "Created Time", + "type_name": "Datetime", + "_source": "finding_info" + } + } + ], + "name": "finding_info", + "description": "The Finding Information object describes metadata related to a security finding generated by a security tool or system.", + "extends": "object", + "profiles": [ + "data_classification", + "datetime" + ], + "caption": "Finding Information" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/firewall_rule.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/firewall_rule.json new file mode 100644 index 00000000..66ae54d3 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/firewall_rule.json @@ -0,0 +1,135 @@ +{ + "attributes": [ + { + "name": { + "type": "string_t", + "description": "The name of the rule that generated the event.", + "requirement": "recommended", + "caption": "Name", + "type_name": "String", + "_source": "rule" + } + }, + { + "type": { + "type": "string_t", + "description": "The rule type.", + "requirement": "optional", + "caption": "Type", + "type_name": "String", + "_source": "rule" + } + }, + { + "version": { + "type": "string_t", + "description": "The rule version. For example: 1.1.", + "requirement": "optional", + "caption": "Version", + "type_name": "String", + "_source": "rule" + } + }, + { + "desc": { + "type": "string_t", + "description": "The description of the rule that generated the event.", + "requirement": "optional", + "caption": "Description", + "type_name": "String", + "_source": "rule" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the rule that generated the event.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "_source": "rule" + } + }, + { + "category": { + "type": "string_t", + "description": "The rule category.", + "requirement": "optional", + "caption": "Category", + "type_name": "String", + "_source": "rule" + } + }, + { + "duration": { + "type": "long_t", + "description": "The rule response time duration, usually used for challenge completion time.", + "requirement": "optional", + "caption": "Duration Milliseconds", + "type_name": "Long", + "_source": "firewall_rule" + } + }, + { + "condition": { + "type": "string_t", + "description": "The rule trigger condition for the rule. For example: SQL_INJECTION.", + "requirement": "optional", + "caption": "Condition", + "type_name": "String", + "_source": "firewall_rule" + } + }, + { + "match_details": { + "type": "string_t", + "description": "The data in a request that rule matched. For example: '[\"10\",\"and\",\"1\"]'.", + "is_array": true, + "requirement": "optional", + "caption": "Match Details", + "type_name": "String", + "_source": "firewall_rule" + } + }, + { + "match_location": { + "type": "string_t", + "description": "The location of the matched data in the source which resulted in the triggered firewall rule. For example: HEADER.", + "requirement": "optional", + "caption": "Match Location", + "type_name": "String", + "_source": "firewall_rule" + } + }, + { + "rate_limit": { + "type": "integer_t", + "description": "The rate limit for a rate-based rule.", + "requirement": "optional", + "caption": "Rate Limit", + "type_name": "Integer", + "_source": "firewall_rule" + } + }, + { + "sensitivity": { + "type": "string_t", + "description": "The sensitivity of the firewall rule in the matched event. For example: HIGH.", + "requirement": "optional", + "caption": "Sensitivity", + "type_name": "String", + "_source": "firewall_rule" + } + } + ], + "name": "firewall_rule", + "description": "The Firewall Rule object represents a specific rule within a firewall policy or event. It contains information about a rule's configuration, properties, and associated actions that define how network traffic is handled by the firewall.", + "extends": "rule", + "constraints": { + "at_least_one": [ + "name", + "uid" + ] + }, + "caption": "Firewall Rule" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_request.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_request.json new file mode 100644 index 00000000..2837168b --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_request.json @@ -0,0 +1,167 @@ +{ + "attributes": [ + { + "args": { + "type": "string_t", + "description": "The arguments sent along with the HTTP request.", + "requirement": "optional", + "caption": "HTTP Arguments", + "type_name": "String", + "_source": "http_request" + } + }, + { + "version": { + "type": "string_t", + "description": "The Hypertext Transfer Protocol (HTTP) version.", + "requirement": "recommended", + "caption": "HTTP Version", + "type_name": "String", + "_source": "http_request" + } + }, + { + "length": { + "type": "integer_t", + "description": "The length of the entire HTTP request, in number of bytes.", + "requirement": "optional", + "caption": "Request Length", + "type_name": "Integer", + "_source": "http_request" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the http request.", + "requirement": "optional", + "caption": "Unique ID", + "type_name": "String", + "_source": "http_request" + } + }, + { + "url": { + "type": "object_t", + "description": "The URL object that pertains to the request.", + "requirement": "recommended", + "caption": "URL", + "object_name": "Uniform Resource Locator", + "object_type": "url", + "_source": "http_request" + } + }, + { + "body_length": { + "type": "integer_t", + "description": "The actual length of the HTTP request body, in number of bytes, independent of a potentially existing Content-Length header.", + "requirement": "optional", + "caption": "Request Body Length", + "type_name": "Integer", + "_source": "http_request" + } + }, + { + "user_agent": { + "type": "string_t", + "description": "The request header that identifies the operating system and web browser.", + "requirement": "recommended", + "caption": "HTTP User-Agent", + "type_name": "String", + "observable": 16, + "_source": "http_request" + } + }, + { + "http_headers": { + "type": "object_t", + "description": "Additional HTTP headers of an HTTP request or response.", + "is_array": true, + "requirement": "recommended", + "caption": "HTTP Headers", + "object_name": "HTTP Header", + "object_type": "http_header", + "_source": "http_request" + } + }, + { + "http_method": { + "type": "string_t", + "enum": { + "OPTIONS": { + "description": "The OPTIONS method describes the communication options for the target resource.", + "caption": "Options" + }, + "GET": { + "description": "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data.", + "caption": "Get" + }, + "HEAD": { + "description": "The HEAD method asks for a response identical to a GET request, but without the response body.", + "caption": "Head" + }, + "POST": { + "description": "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.", + "caption": "Post" + }, + "PUT": { + "description": "The PUT method replaces all current representations of the target resource with the request payload.", + "caption": "Put" + }, + "DELETE": { + "description": "The DELETE method deletes the specified resource.", + "caption": "Delete" + }, + "TRACE": { + "description": "The TRACE method performs a message loop-back test along the path to the target resource.", + "caption": "Trace" + }, + "CONNECT": { + "description": "The CONNECT method establishes a tunnel to the server identified by the target resource.", + "caption": "Connect" + }, + "PATCH": { + "description": "The PATCH method applies partial modifications to a resource.", + "caption": "Patch" + } + }, + "description": "The HTTP request method indicates the desired action to be performed for a given resource.", + "requirement": "recommended", + "caption": "HTTP Method", + "type_name": "String", + "_source": "http_request" + } + }, + { + "referrer": { + "type": "string_t", + "description": "The request header that identifies the address of the previous web page, which is linked to the current web page or resource being requested.", + "requirement": "optional", + "caption": "HTTP Referrer", + "type_name": "String", + "_source": "http_request" + } + }, + { + "x_forwarded_for": { + "type": "ip_t", + "description": "The X-Forwarded-For header identifying the originating IP address(es) of a client connecting to a web server through an HTTP proxy or a load balancer.", + "is_array": true, + "requirement": "optional", + "caption": "X-Forwarded-For", + "type_name": "IP Address", + "_source": "http_request" + } + } + ], + "name": "http_request", + "description": "The HTTP Request object represents the attributes of a request made to a web server. It encapsulates the details and metadata associated with an HTTP request, including the request method, headers, URL, query parameters, body content, and other relevant information.", + "extends": "object", + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:OutboundInternetNetworkTraffic.", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:OutboundInternetNetworkTraffic/" + } + ], + "caption": "HTTP Request" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_response.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_response.json new file mode 100644 index 00000000..65c5f003 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/http_response.json @@ -0,0 +1,96 @@ +{ + "attributes": [ + { + "code": { + "type": "integer_t", + "description": "The Hypertext Transfer Protocol (HTTP) status code returned from the web server to the client. For example, 200.", + "requirement": "required", + "caption": "Response Code", + "type_name": "Integer", + "_source": "http_response" + } + }, + { + "message": { + "type": "string_t", + "description": "The description of the event/finding, as defined by the source.", + "requirement": "optional", + "caption": "Message", + "type_name": "String", + "_source": "http_response" + } + }, + { + "status": { + "type": "string_t", + "description": "The response status. For example: A successful HTTP status of 'OK' which corresponds to a code of 200.", + "requirement": "optional", + "caption": "Status", + "type_name": "String", + "_source": "http_response" + } + }, + { + "length": { + "type": "integer_t", + "description": "The length of the entire HTTP response, in number of bytes.", + "requirement": "optional", + "caption": "Response Length", + "type_name": "Integer", + "_source": "http_response" + } + }, + { + "content_type": { + "type": "string_t", + "description": "The request header that identifies the original media type of the resource (prior to any content encoding applied for sending).", + "requirement": "optional", + "caption": "HTTP Content Type", + "type_name": "String", + "_source": "http_response" + } + }, + { + "body_length": { + "type": "integer_t", + "description": "The actual length of the HTTP response body, in number of bytes, independent of a potentially existing Content-Length header.", + "requirement": "optional", + "caption": "Response Body Length", + "type_name": "Integer", + "_source": "http_response" + } + }, + { + "http_headers": { + "type": "object_t", + "description": "Additional HTTP headers of an HTTP request or response.", + "is_array": true, + "requirement": "recommended", + "caption": "HTTP Headers", + "object_name": "HTTP Header", + "object_type": "http_header", + "_source": "http_response" + } + }, + { + "latency": { + "type": "integer_t", + "description": "The HTTP response latency measured in milliseconds.", + "requirement": "optional", + "caption": "Latency", + "type_name": "Integer", + "_source": "http_response" + } + } + ], + "name": "http_response", + "description": "The HTTP Response object contains detailed information about the response sent from a web server to the requester. It encompasses attributes and metadata that describe the response status, headers, body content, and other relevant information.", + "extends": "object", + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:InboundInternetNetworkTraffic.", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:InboundInternetNetworkTraffic/" + } + ], + "caption": "HTTP Response" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/metadata.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/metadata.json new file mode 100644 index 00000000..9afd440a --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/metadata.json @@ -0,0 +1,431 @@ +{ + "attributes": [ + { + "untruncated_size": { + "type": "integer_t", + "description": "The original size of the OCSF event data in kilobytes before any truncation occurred. This field is typically populated when is_truncated is true to indicate the full size of the original event.", + "requirement": "optional", + "caption": "Untruncated Size", + "type_name": "Integer", + "_source": "metadata" + } + }, + { + "profiles": { + "type": "string_t", + "description": "The list of profiles used to create the event. Profiles should be referenced by their name attribute for core profiles, or extension/name for profiles from extensions.", + "is_array": true, + "requirement": "optional", + "caption": "Profiles", + "type_name": "String", + "_source": "metadata" + } + }, + { + "loggers": { + "type": "object_t", + "description": "An array of Logger objects that describe the pipeline of devices and logging products between the event source and its eventual destination. Note, this attribute can be used when there is a complex end-to-end path of event flow and/or to track the chain of custody of the data.", + "is_array": true, + "requirement": "optional", + "caption": "Loggers", + "object_name": "Logger", + "object_type": "logger", + "_source": "metadata" + } + }, + { + "type": { + "type": "string_t", + "description": "The type of the event or finding as a subset of the source of the event. This can be any distinguishing characteristic of the data. For example 'Management Events' or 'Device Penetration Test'.", + "requirement": "optional", + "caption": "Type", + "type_name": "String", + "_source": "metadata" + } + }, + { + "processed_time": { + "type": "timestamp_t", + "description": "The event processed time, such as an ETL operation.", + "requirement": "optional", + "caption": "Processed Time", + "type_name": "Timestamp", + "_source": "metadata" + } + }, + { + "is_truncated": { + "type": "boolean_t", + "description": "Indicates whether the OCSF event data has been truncated due to size limitations. When true, some event data may have been omitted to fit within system constraints.", + "requirement": "optional", + "caption": "Is Truncated", + "type_name": "Boolean", + "_source": "metadata" + } + }, + { + "transformation_info_list": { + "type": "object_t", + "description": "An array of transformation info that describes the mappings or transforms applied to the data.", + "is_array": true, + "requirement": "optional", + "caption": "Transformation Info", + "object_name": "Transformation Info", + "object_type": "transformation_info", + "_source": "metadata" + } + }, + { + "original_event_uid": { + "type": "string_t", + "description": "The unique identifier assigned to the event in its original logging system before transformation to OCSF format. This field preserves the source system's native event identifier, enabling traceability back to the raw log entry. For example, a Windows Event Record ID, a syslog message ID, a Splunk _cd value, or a database transaction log sequence number.", + "requirement": "optional", + "caption": "Original Event ID", + "type_name": "String", + "_source": "metadata" + } + }, + { + "modified_time": { + "type": "timestamp_t", + "description": "The time when the event was last modified or enriched.", + "requirement": "optional", + "caption": "Modified Time", + "type_name": "Timestamp", + "_source": "metadata" + } + }, + { + "data_classification": { + "profile": "data_classification", + "type": "object_t", + "description": "The Data Classification object includes information about data classification levels and data category types.", + "group": "context", + "requirement": "recommended", + "caption": "Data Classification", + "object_name": "Data Classification", + "object_type": "data_classification", + "@deprecated": { + "message": "Use the attribute data_classifications instead", + "since": "1.4.0" + }, + "_source": "metadata" + } + }, + { + "version": { + "type": "string_t", + "description": "The version of the OCSF schema, using Semantic Versioning Specification (SemVer). For example: 1.0.0. Event consumers use the version to determine the available event attributes.", + "requirement": "required", + "caption": "Version", + "type_name": "String", + "_source": "metadata" + } + }, + { + "modified_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the event was last modified or enriched.", + "requirement": "optional", + "caption": "Modified Time", + "type_name": "Datetime", + "_source": "metadata" + } + }, + { + "original_time": { + "type": "string_t", + "description": "The original event time as reported by the event source. For example, the time in the original format from system event log such as Syslog on Unix/Linux and the System event file on Windows. Omit if event is generated instead of collected via logs.", + "requirement": "recommended", + "caption": "Original Time", + "type_name": "String", + "_source": "metadata" + } + }, + { + "reporter": { + "type": "object_t", + "description": "The entity from which the event or finding was first reported.", + "requirement": "recommended", + "caption": "Reporter", + "object_name": "Reporter", + "object_type": "reporter", + "_source": "metadata" + } + }, + { + "extension": { + "type": "object_t", + "description": "The schema extension used to create the event.", + "requirement": "optional", + "caption": "Schema Extension", + "object_name": "Schema Extension", + "object_type": "extension", + "@deprecated": { + "message": "Use the extensions attribute instead.", + "since": "1.1.0" + }, + "_source": "metadata" + } + }, + { + "logged_time": { + "type": "timestamp_t", + "description": "

The time when the logging system collected and logged the event.

This attribute is distinct from the event time in that event time typically contain the time extracted from the original event. Most of the time, these two times will be different.", + "requirement": "optional", + "caption": "Logged Time", + "type_name": "Timestamp", + "_source": "metadata" + } + }, + { + "debug": { + "type": "string_t", + "description": "Debug information about non-fatal issues with this OCSF event. Each issue is a line in this string array.", + "is_array": true, + "requirement": "optional", + "caption": "Debug Information", + "type_name": "String", + "_source": "metadata" + } + }, + { + "processed_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The event processed time, such as an ETL operation.", + "requirement": "optional", + "caption": "Processed Time", + "type_name": "Datetime", + "_source": "metadata" + } + }, + { + "source": { + "type": "string_t", + "description": "The source of the event or finding. This can be any distinguishing name for the logical origin of the data \u2014 for example, 'CloudTrail Events', or a use case like 'Attack Simulations' or 'Vulnerability Scans'.", + "requirement": "optional", + "caption": "Source", + "type_name": "String", + "_source": "metadata" + } + }, + { + "event_code": { + "type": "string_t", + "description": "The identifier of the original event. For example the numerical Windows Event Code or Cisco syslog code.", + "requirement": "optional", + "caption": "Event Code", + "type_name": "String", + "_source": "metadata" + } + }, + { + "log_version": { + "type": "string_t", + "description": "The event log schema version of the original event. For example the syslog version or the Cisco Log Schema version", + "requirement": "optional", + "caption": "Log Version", + "type_name": "String", + "_source": "metadata" + } + }, + { + "labels": { + "type": "string_t", + "description": "The list of labels attached to the event. For example: [\"sample\", \"dev\"]", + "is_array": true, + "requirement": "optional", + "caption": "Labels", + "type_name": "String", + "_source": "metadata" + } + }, + { + "log_provider": { + "type": "string_t", + "description": "The logging provider or logging service that logged the event. For example AWS CloudWatch or Splunk.", + "requirement": "optional", + "caption": "Log Provider", + "type_name": "String", + "_source": "metadata" + } + }, + { + "data_classifications": { + "profile": "data_classification", + "type": "object_t", + "description": "A list of Data Classification objects, that include information about data classification levels and data category types, identified by a classifier.", + "group": "context", + "is_array": true, + "requirement": "recommended", + "caption": "Data Classification", + "object_name": "Data Classification", + "object_type": "data_classification", + "_source": "metadata" + } + }, + { + "log_source": { + "type": "string_t", + "description": "The log system or component where the data originated. For example, a file path, syslog server name or a Windows hostname and logging subsystem such as Security.", + "requirement": "optional", + "caption": "Log Source", + "type_name": "String", + "_source": "metadata" + } + }, + { + "extensions": { + "type": "object_t", + "description": "The schema extensions used to create the event.", + "is_array": true, + "requirement": "optional", + "caption": "Schema Extensions", + "object_name": "Schema Extension", + "object_type": "extension", + "_source": "metadata" + } + }, + { + "transmit_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the event was transmitted from the logging device to it's next destination.", + "requirement": "optional", + "caption": "Transmission Time", + "type_name": "Datetime", + "_source": "metadata" + } + }, + { + "logged_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "

The time when the logging system collected and logged the event.

This attribute is distinct from the event time in that event time typically contain the time extracted from the original event. Most of the time, these two times will be different.", + "requirement": "optional", + "caption": "Logged Time", + "type_name": "Datetime", + "_source": "metadata" + } + }, + { + "correlation_uid": { + "type": "string_t", + "description": "A unique identifier used to correlate this OCSF event with other related OCSF events, distinct from the event's uid value. This enables linking multiple OCSF events that are part of the same activity, transaction, or security incident across different systems or time periods.", + "requirement": "optional", + "caption": "Correlation UID", + "type_name": "String", + "_source": "metadata" + } + }, + { + "uid": { + "type": "string_t", + "description": "A unique identifier assigned to the OCSF event. This ID is specific to the OCSF event itself and is distinct from the original event identifier in the source system (see original_event_uid).", + "requirement": "optional", + "caption": "Event UID", + "type_name": "String", + "_source": "metadata" + } + }, + { + "tags": { + "type": "object_t", + "description": "The list of tags; {key:value} pairs associated to the event.", + "is_array": true, + "requirement": "optional", + "caption": "Tags", + "object_name": "Key:Value object", + "object_type": "key_value_object", + "_source": "metadata" + } + }, + { + "log_level": { + "type": "string_t", + "description": "The level at which an event was logged. This can be log provider specific. For example the audit level.", + "requirement": "optional", + "caption": "Log Level", + "type_name": "String", + "_source": "metadata" + } + }, + { + "transmit_time": { + "type": "timestamp_t", + "description": "The time when the event was transmitted from the logging device to it's next destination.", + "requirement": "optional", + "caption": "Transmission Time", + "type_name": "Timestamp", + "_source": "metadata" + } + }, + { + "tenant_uid": { + "type": "string_t", + "description": "The unique tenant identifier.", + "requirement": "recommended", + "caption": "Tenant UID", + "type_name": "String", + "_source": "metadata" + } + }, + { + "sequence": { + "type": "integer_t", + "description": "Sequence number of the event. The sequence number is a value available in some events, to make the exact ordering of events unambiguous, regardless of the event time precision.", + "requirement": "optional", + "caption": "Sequence Number", + "type_name": "Integer", + "_source": "metadata" + } + }, + { + "product": { + "type": "object_t", + "description": "The product that reported the event.", + "requirement": "required", + "caption": "Product", + "object_name": "Product", + "object_type": "product", + "_source": "metadata" + } + }, + { + "log_name": { + "type": "string_t", + "description": "The event log name, typically for the consumer of the event. For example, the storage bucket name, SIEM repository index name, etc.", + "requirement": "recommended", + "caption": "Log Name", + "type_name": "String", + "_source": "metadata" + } + }, + { + "log_format": { + "type": "string_t", + "description": "The format of data in the log where the data originated. For example CSV, XML, Windows Multiline, JSON, syslog or Cisco Log Schema.", + "requirement": "optional", + "caption": "Log Source Format", + "type_name": "String", + "_source": "metadata" + } + } + ], + "name": "metadata", + "description": "The Metadata object describes the metadata associated with the event.", + "extends": "object", + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:Metadata", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:Metadata/" + } + ], + "profiles": [ + "data_classification", + "datetime" + ], + "caption": "Metadata" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_endpoint.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_endpoint.json new file mode 100644 index 00000000..fe97c820 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_endpoint.json @@ -0,0 +1,448 @@ +{ + "attributes": [ + { + "name": { + "type": "string_t", + "description": "The short name of the endpoint.", + "requirement": "recommended", + "caption": "Name", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "owner": { + "type": "object_t", + "description": "The identity of the service or user account that owns the endpoint or was last logged into it.", + "requirement": "recommended", + "caption": "Owner", + "object_name": "User", + "object_type": "user", + "_source": "endpoint" + } + }, + { + "port": { + "type": "port_t", + "description": "The port used for communication within the network connection.", + "requirement": "recommended", + "caption": "Port", + "type_name": "Port", + "_source": "network_endpoint" + } + }, + { + "type": { + "type": "string_t", + "description": "The network endpoint type. For example: unknown, server, desktop, laptop, tablet, mobile, virtual, browser, or other.", + "requirement": "optional", + "caption": "Type", + "type_name": "String", + "_source": "network_endpoint", + "_sibling_of": "type_id" + } + }, + { + "os": { + "type": "object_t", + "description": "The endpoint operating system.", + "requirement": "optional", + "caption": "OS", + "object_name": "Operating System (OS)", + "object_type": "os", + "_source": "endpoint" + } + }, + { + "domain": { + "type": "string_t", + "description": "The name of the domain that the endpoint belongs to or that corresponds to the endpoint.", + "requirement": "optional", + "caption": "Domain", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "ip": { + "type": "ip_t", + "description": "The IP address of the endpoint, in either IPv4 or IPv6 format.", + "requirement": "recommended", + "caption": "IP Address", + "type_name": "IP Address", + "_source": "endpoint" + } + }, + { + "location": { + "type": "object_t", + "description": "The geographical location of the endpoint.", + "requirement": "optional", + "caption": "Geo Location", + "object_name": "Geo Location", + "object_type": "location", + "_source": "endpoint" + } + }, + { + "hostname": { + "type": "hostname_t", + "description": "The fully qualified name of the endpoint.", + "requirement": "recommended", + "caption": "Hostname", + "type_name": "Hostname", + "_source": "endpoint" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the endpoint.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "observable": 48, + "_source": "network_endpoint" + } + }, + { + "mac": { + "type": "mac_t", + "description": "The Media Access Control (MAC) address of the endpoint.", + "requirement": "optional", + "caption": "MAC Address", + "type_name": "MAC Address", + "_source": "endpoint" + } + }, + { + "type_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "A laptop computer.", + "caption": "Laptop" + }, + "6": { + "description": "A virtual machine.", + "caption": "Virtual" + }, + "0": { + "description": "The type is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A server.", + "caption": "Server" + }, + "2": { + "description": "A desktop computer.", + "caption": "Desktop" + }, + "99": { + "description": "The type is not mapped. See the type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A tablet computer.", + "caption": "Tablet" + }, + "5": { + "description": "A mobile phone.", + "caption": "Mobile" + }, + "7": { + "description": "An IOT (Internet of Things) device.", + "caption": "IOT" + }, + "8": { + "description": "A web browser.", + "caption": "Browser" + }, + "9": { + "description": "A networking firewall.", + "caption": "Firewall" + }, + "10": { + "description": "A networking switch.", + "caption": "Switch" + }, + "11": { + "description": "A networking hub.", + "caption": "Hub" + }, + "12": { + "description": "A networking router.", + "caption": "Router" + }, + "14": { + "description": "An intrusion prevention system.", + "caption": "IPS" + }, + "15": { + "description": "A Load Balancer device.", + "caption": "Load Balancer" + }, + "13": { + "description": "An intrusion detection system.", + "caption": "IDS" + } + }, + "description": "The network endpoint type ID.", + "requirement": "recommended", + "caption": "Type ID", + "type_name": "Integer", + "sibling": "type", + "_source": "network_endpoint" + } + }, + { + "agent_list": { + "type": "object_t", + "description": "A list of agent objects associated with a device, endpoint, or resource.", + "is_array": true, + "requirement": "optional", + "caption": "Agent List", + "object_name": "Agent", + "object_type": "agent", + "_source": "endpoint" + } + }, + { + "autonomous_system": { + "type": "object_t", + "description": "The Autonomous System details associated with an IP address.", + "requirement": "optional", + "caption": "Autonomous System", + "object_name": "Autonomous System", + "object_type": "autonomous_system", + "_source": "network_endpoint" + } + }, + { + "container": { + "profile": "container", + "type": "object_t", + "description": "The information describing an instance of a container. A container is a prepackaged, portable system image that runs isolated on an existing system using a container runtime like containerd.", + "group": "context", + "requirement": "recommended", + "caption": "Container", + "object_name": "Container", + "object_type": "container", + "_source": "endpoint" + } + }, + { + "hw_info": { + "type": "object_t", + "description": "The endpoint hardware information.", + "requirement": "optional", + "caption": "Hardware Info", + "object_name": "Device Hardware Info", + "object_type": "device_hw_info", + "_source": "endpoint" + } + }, + { + "instance_uid": { + "type": "string_t", + "description": "The unique identifier of a VM instance.", + "requirement": "recommended", + "caption": "Instance ID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "interface_name": { + "type": "string_t", + "description": "The name of the network interface (e.g. eth2).", + "requirement": "recommended", + "caption": "Network Interface Name", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "interface_uid": { + "type": "string_t", + "description": "The unique identifier of the network interface.", + "requirement": "recommended", + "caption": "Network Interface ID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "intermediate_ips": { + "type": "ip_t", + "description": "The intermediate IP Addresses. For example, the IP addresses in the HTTP X-Forwarded-For header.", + "is_array": true, + "requirement": "optional", + "caption": "Intermediate IP Addresses", + "type_name": "IP Address", + "_source": "network_endpoint" + } + }, + { + "isp": { + "type": "string_t", + "description": "The name of the Internet Service Provider (ISP).", + "requirement": "optional", + "caption": "ISP Name", + "type_name": "String", + "_source": "network_endpoint" + } + }, + { + "isp_org": { + "type": "string_t", + "description": "The organization name of the Internet Service Provider (ISP). This represents the parent organization or company that owns/operates the ISP. For example, Comcast Corporation would be the ISP org for Xfinity internet service. This attribute helps identify the ultimate provider when ISPs operate under different brand names.", + "requirement": "optional", + "caption": "ISP Org", + "type_name": "String", + "_source": "network_endpoint" + } + }, + { + "namespace_pid": { + "profile": "container", + "type": "integer_t", + "description": "If running under a process namespace (such as in a container), the process identifier within that process namespace.", + "group": "context", + "requirement": "recommended", + "caption": "Namespace PID", + "type_name": "Integer", + "_source": "endpoint" + } + }, + { + "network_scope": { + "type": "string_t", + "description": "Indicates whether the endpoint resides inside the customer\u2019s network, outside on the Internet, or if its location relative to the customer\u2019s network cannot be determined. The value is normalized to the caption of the network_scope_id.", + "requirement": "optional", + "caption": "Network Scope", + "type_name": "String", + "_source": "network_endpoint", + "_sibling_of": "network_scope_id" + } + }, + { + "network_scope_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "Unknown whether this endpoint resides within the customer\u2019s network.", + "caption": "Unknown" + }, + "1": { + "description": "The endpoint resides inside the customer\u2019s network.", + "caption": "Internal" + }, + "2": { + "description": "The endpoint is on the Internet or otherwise external to the customer\u2019s network.", + "caption": "External" + }, + "99": { + "description": "The network scope is not mapped. See the network_scope attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the endpoint\u2019s network scope. The normalized network scope identifier indicates whether the endpoint resides inside the customer\u2019s network, outside on the Internet, or if its location relative to the customer\u2019s network cannot be determined.", + "requirement": "optional", + "caption": "Network Scope ID", + "type_name": "Integer", + "sibling": "network_scope", + "_source": "network_endpoint" + } + }, + { + "proxy_endpoint": { + "type": "object_t", + "description": "The network proxy information pertaining to a specific endpoint. This can be used to describe information pertaining to network address translation (NAT).", + "requirement": "optional", + "caption": "Proxy Endpoint", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "_source": "network_endpoint" + } + }, + { + "subnet_uid": { + "type": "string_t", + "description": "The unique identifier of a virtual subnet.", + "requirement": "optional", + "caption": "Subnet UID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "svc_name": { + "type": "string_t", + "description": "The service name in service-to-service connections. For example, AWS VPC logs the pkt-src-aws-service and pkt-dst-aws-service fields identify the connection is coming from or going to an AWS service.", + "requirement": "recommended", + "caption": "Service Name", + "type_name": "String", + "_source": "network_endpoint" + } + }, + { + "vlan_uid": { + "type": "string_t", + "description": "The Virtual LAN identifier.", + "requirement": "optional", + "caption": "VLAN", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "vpc_uid": { + "type": "string_t", + "description": "The unique identifier of the Virtual Private Cloud (VPC).", + "requirement": "optional", + "caption": "VPC UID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "zone": { + "type": "string_t", + "description": "The network zone or LAN segment.", + "requirement": "optional", + "caption": "Network Zone", + "type_name": "String", + "_source": "endpoint" + } + } + ], + "name": "network_endpoint", + "description": "The Network Endpoint object describes characteristics of a network endpoint. These can be a source or destination of a network connection.", + "extends": "endpoint", + "constraints": { + "at_least_one": [ + "ip", + "uid", + "name", + "hostname", + "svc_name", + "instance_uid", + "interface_uid", + "interface_name", + "domain" + ] + }, + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:ComputerNetworkNode.", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:ComputerNetworkNode/" + } + ], + "profiles": [ + "container" + ], + "caption": "Network Endpoint", + "observable": 20 +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_proxy.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_proxy.json new file mode 100644 index 00000000..f7225c9c --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/network_proxy.json @@ -0,0 +1,448 @@ +{ + "attributes": [ + { + "name": { + "type": "string_t", + "description": "The short name of the endpoint.", + "requirement": "recommended", + "caption": "Name", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "owner": { + "type": "object_t", + "description": "The identity of the service or user account that owns the endpoint or was last logged into it.", + "requirement": "recommended", + "caption": "Owner", + "object_name": "User", + "object_type": "user", + "_source": "endpoint" + } + }, + { + "port": { + "type": "port_t", + "description": "The port used for communication within the network connection.", + "requirement": "recommended", + "caption": "Port", + "type_name": "Port", + "_source": "network_endpoint" + } + }, + { + "type": { + "type": "string_t", + "description": "The network endpoint type. For example: unknown, server, desktop, laptop, tablet, mobile, virtual, browser, or other.", + "requirement": "optional", + "caption": "Type", + "type_name": "String", + "_source": "network_endpoint", + "_sibling_of": "type_id" + } + }, + { + "os": { + "type": "object_t", + "description": "The endpoint operating system.", + "requirement": "optional", + "caption": "OS", + "object_name": "Operating System (OS)", + "object_type": "os", + "_source": "endpoint" + } + }, + { + "domain": { + "type": "string_t", + "description": "The name of the domain that the endpoint belongs to or that corresponds to the endpoint.", + "requirement": "optional", + "caption": "Domain", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "ip": { + "type": "ip_t", + "description": "The IP address of the endpoint, in either IPv4 or IPv6 format.", + "requirement": "recommended", + "caption": "IP Address", + "type_name": "IP Address", + "_source": "endpoint" + } + }, + { + "location": { + "type": "object_t", + "description": "The geographical location of the endpoint.", + "requirement": "optional", + "caption": "Geo Location", + "object_name": "Geo Location", + "object_type": "location", + "_source": "endpoint" + } + }, + { + "hostname": { + "type": "hostname_t", + "description": "The fully qualified name of the endpoint.", + "requirement": "recommended", + "caption": "Hostname", + "type_name": "Hostname", + "_source": "endpoint" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the endpoint.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "observable": 48, + "_source": "network_endpoint" + } + }, + { + "mac": { + "type": "mac_t", + "description": "The Media Access Control (MAC) address of the endpoint.", + "requirement": "optional", + "caption": "MAC Address", + "type_name": "MAC Address", + "_source": "endpoint" + } + }, + { + "type_id": { + "type": "integer_t", + "enum": { + "3": { + "description": "A laptop computer.", + "caption": "Laptop" + }, + "6": { + "description": "A virtual machine.", + "caption": "Virtual" + }, + "0": { + "description": "The type is unknown.", + "caption": "Unknown" + }, + "1": { + "description": "A server.", + "caption": "Server" + }, + "2": { + "description": "A desktop computer.", + "caption": "Desktop" + }, + "99": { + "description": "The type is not mapped. See the type attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "description": "A tablet computer.", + "caption": "Tablet" + }, + "5": { + "description": "A mobile phone.", + "caption": "Mobile" + }, + "7": { + "description": "An IOT (Internet of Things) device.", + "caption": "IOT" + }, + "8": { + "description": "A web browser.", + "caption": "Browser" + }, + "9": { + "description": "A networking firewall.", + "caption": "Firewall" + }, + "10": { + "description": "A networking switch.", + "caption": "Switch" + }, + "11": { + "description": "A networking hub.", + "caption": "Hub" + }, + "12": { + "description": "A networking router.", + "caption": "Router" + }, + "14": { + "description": "An intrusion prevention system.", + "caption": "IPS" + }, + "15": { + "description": "A Load Balancer device.", + "caption": "Load Balancer" + }, + "13": { + "description": "An intrusion detection system.", + "caption": "IDS" + } + }, + "description": "The network endpoint type ID.", + "requirement": "recommended", + "caption": "Type ID", + "type_name": "Integer", + "sibling": "type", + "_source": "network_endpoint" + } + }, + { + "agent_list": { + "type": "object_t", + "description": "A list of agent objects associated with a device, endpoint, or resource.", + "is_array": true, + "requirement": "optional", + "caption": "Agent List", + "object_name": "Agent", + "object_type": "agent", + "_source": "endpoint" + } + }, + { + "autonomous_system": { + "type": "object_t", + "description": "The Autonomous System details associated with an IP address.", + "requirement": "optional", + "caption": "Autonomous System", + "object_name": "Autonomous System", + "object_type": "autonomous_system", + "_source": "network_endpoint" + } + }, + { + "container": { + "profile": "container", + "type": "object_t", + "description": "The information describing an instance of a container. A container is a prepackaged, portable system image that runs isolated on an existing system using a container runtime like containerd.", + "group": "context", + "requirement": "recommended", + "caption": "Container", + "object_name": "Container", + "object_type": "container", + "_source": "endpoint" + } + }, + { + "hw_info": { + "type": "object_t", + "description": "The endpoint hardware information.", + "requirement": "optional", + "caption": "Hardware Info", + "object_name": "Device Hardware Info", + "object_type": "device_hw_info", + "_source": "endpoint" + } + }, + { + "instance_uid": { + "type": "string_t", + "description": "The unique identifier of a VM instance.", + "requirement": "recommended", + "caption": "Instance ID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "interface_name": { + "type": "string_t", + "description": "The name of the network interface (e.g. eth2).", + "requirement": "recommended", + "caption": "Network Interface Name", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "interface_uid": { + "type": "string_t", + "description": "The unique identifier of the network interface.", + "requirement": "recommended", + "caption": "Network Interface ID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "intermediate_ips": { + "type": "ip_t", + "description": "The intermediate IP Addresses. For example, the IP addresses in the HTTP X-Forwarded-For header.", + "is_array": true, + "requirement": "optional", + "caption": "Intermediate IP Addresses", + "type_name": "IP Address", + "_source": "network_endpoint" + } + }, + { + "isp": { + "type": "string_t", + "description": "The name of the Internet Service Provider (ISP).", + "requirement": "optional", + "caption": "ISP Name", + "type_name": "String", + "_source": "network_endpoint" + } + }, + { + "isp_org": { + "type": "string_t", + "description": "The organization name of the Internet Service Provider (ISP). This represents the parent organization or company that owns/operates the ISP. For example, Comcast Corporation would be the ISP org for Xfinity internet service. This attribute helps identify the ultimate provider when ISPs operate under different brand names.", + "requirement": "optional", + "caption": "ISP Org", + "type_name": "String", + "_source": "network_endpoint" + } + }, + { + "namespace_pid": { + "profile": "container", + "type": "integer_t", + "description": "If running under a process namespace (such as in a container), the process identifier within that process namespace.", + "group": "context", + "requirement": "recommended", + "caption": "Namespace PID", + "type_name": "Integer", + "_source": "endpoint" + } + }, + { + "network_scope": { + "type": "string_t", + "description": "Indicates whether the endpoint resides inside the customer\u2019s network, outside on the Internet, or if its location relative to the customer\u2019s network cannot be determined. The value is normalized to the caption of the network_scope_id.", + "requirement": "optional", + "caption": "Network Scope", + "type_name": "String", + "_source": "network_endpoint", + "_sibling_of": "network_scope_id" + } + }, + { + "network_scope_id": { + "type": "integer_t", + "enum": { + "0": { + "description": "Unknown whether this endpoint resides within the customer\u2019s network.", + "caption": "Unknown" + }, + "1": { + "description": "The endpoint resides inside the customer\u2019s network.", + "caption": "Internal" + }, + "2": { + "description": "The endpoint is on the Internet or otherwise external to the customer\u2019s network.", + "caption": "External" + }, + "99": { + "description": "The network scope is not mapped. See the network_scope attribute, which contains a data source specific value.", + "caption": "Other" + } + }, + "description": "The normalized identifier of the endpoint\u2019s network scope. The normalized network scope identifier indicates whether the endpoint resides inside the customer\u2019s network, outside on the Internet, or if its location relative to the customer\u2019s network cannot be determined.", + "requirement": "optional", + "caption": "Network Scope ID", + "type_name": "Integer", + "sibling": "network_scope", + "_source": "network_endpoint" + } + }, + { + "proxy_endpoint": { + "type": "object_t", + "description": "The network proxy information pertaining to a specific endpoint. This can be used to describe information pertaining to network address translation (NAT).", + "requirement": "optional", + "caption": "Proxy Endpoint", + "object_name": "Network Proxy Endpoint", + "object_type": "network_proxy", + "_source": "network_endpoint" + } + }, + { + "subnet_uid": { + "type": "string_t", + "description": "The unique identifier of a virtual subnet.", + "requirement": "optional", + "caption": "Subnet UID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "svc_name": { + "type": "string_t", + "description": "The service name in service-to-service connections. For example, AWS VPC logs the pkt-src-aws-service and pkt-dst-aws-service fields identify the connection is coming from or going to an AWS service.", + "requirement": "recommended", + "caption": "Service Name", + "type_name": "String", + "_source": "network_endpoint" + } + }, + { + "vlan_uid": { + "type": "string_t", + "description": "The Virtual LAN identifier.", + "requirement": "optional", + "caption": "VLAN", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "vpc_uid": { + "type": "string_t", + "description": "The unique identifier of the Virtual Private Cloud (VPC).", + "requirement": "optional", + "caption": "VPC UID", + "type_name": "String", + "_source": "endpoint" + } + }, + { + "zone": { + "type": "string_t", + "description": "The network zone or LAN segment.", + "requirement": "optional", + "caption": "Network Zone", + "type_name": "String", + "_source": "endpoint" + } + } + ], + "name": "network_proxy", + "description": "The network proxy endpoint object describes a proxy server, which acts as an intermediary between a client requesting a resource and the server providing that resource.", + "extends": "network_endpoint", + "constraints": { + "at_least_one": [ + "ip", + "uid", + "name", + "hostname", + "svc_name", + "instance_uid", + "interface_uid", + "interface_name", + "domain" + ] + }, + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:ProxyServer", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:ProxyServer/" + } + ], + "profiles": [ + "container" + ], + "caption": "Network Proxy Endpoint", + "observable": 20 +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/process.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/process.json new file mode 100644 index 00000000..148e47e1 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/process.json @@ -0,0 +1,446 @@ +{ + "attributes": [ + { + "name": { + "type": "process_name_t", + "description": "The friendly name of the process, for example: Notepad++.", + "requirement": "recommended", + "caption": "Name", + "type_name": "String", + "_source": "process_entity" + } + }, + { + "pid": { + "type": "integer_t", + "description": "The process identifier, as reported by the operating system. Process ID (PID) is a number used by the operating system to uniquely identify an active process.", + "requirement": "recommended", + "caption": "Process ID", + "type_name": "Integer", + "observable": 15, + "_source": "process_entity" + } + }, + { + "session": { + "type": "object_t", + "description": "The user session under which this process is running.", + "requirement": "optional", + "caption": "Session", + "object_name": "Session", + "object_type": "session", + "_source": "process" + } + }, + { + "file": { + "type": "object_t", + "description": "The process file object.", + "requirement": "recommended", + "caption": "File", + "object_name": "File", + "object_type": "file", + "_source": "process" + } + }, + { + "user": { + "type": "object_t", + "description": "The user under which this process is running.", + "requirement": "recommended", + "caption": "User", + "object_name": "User", + "object_type": "user", + "_source": "process" + } + }, + { + "path": { + "type": "string_t", + "description": "The process file path.", + "requirement": "optional", + "caption": "Path", + "type_name": "String", + "_source": "process_entity" + } + }, + { + "group": { + "profile": "linux/linux_users", + "type": "object_t", + "description": "The group under which this process is running.", + "requirement": "recommended", + "caption": "Group", + "object_name": "Group", + "object_type": "group", + "_source": "linux/process", + "_source_patched": "process" + } + }, + { + "tid": { + "type": "integer_t", + "description": "The identifier of the thread associated with the event, as returned by the operating system.", + "requirement": "optional", + "caption": "Thread ID", + "type_name": "Integer", + "@deprecated": { + "message": "tid is deprecated in favor of ptid. ptid has type long_t which can accommodate the thread identifiers returned by all platforms (e.g. 64-bit on MacOS).", + "since": "1.6.0" + }, + "_source": "process" + } + }, + { + "uid": { + "type": "string_t", + "description": "A unique identifier for this process assigned by the producer (tool). Facilitates correlation of a process event with other events for that process.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "observable": 39, + "_source": "process_entity" + } + }, + { + "loaded_modules": { + "type": "string_t", + "description": "The list of loaded module names.", + "is_array": true, + "requirement": "optional", + "caption": "Loaded Modules", + "type_name": "String", + "_source": "process" + } + }, + { + "ancestry": { + "type": "object_t", + "description": "An array of Process Entities describing the extended parentage of this process object. Direct parent information should be expressed through the parent_process attribute. The first array element is the direct parent of this process object. Subsequent list elements go up the process parentage hierarchy. That is, the array is sorted from newest to oldest process. It is recommended to only populate this field for the top-level process object.", + "is_array": true, + "references": [ + { + "description": "Guidance on Representing Process Parentage", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/representing-process-parentage.md" + } + ], + "requirement": "optional", + "caption": "Ancestry", + "object_name": "Process Entity", + "object_type": "process_entity", + "_source": "process" + } + }, + { + "cmd_line": { + "type": "string_t", + "description": "The full command line used to launch an application, service, process, or job. For example: ssh user@10.0.0.10. If the command line is unavailable or missing, the empty string '' is to be used.", + "requirement": "recommended", + "caption": "Command Line", + "type_name": "String", + "observable": 13, + "_source": "process_entity" + } + }, + { + "container": { + "profile": "container", + "type": "object_t", + "description": "The information describing an instance of a container. A container is a prepackaged, portable system image that runs isolated on an existing system using a container runtime like containerd.", + "group": "context", + "requirement": "recommended", + "caption": "Container", + "object_name": "Container", + "object_type": "container", + "_source": "process" + } + }, + { + "cpid": { + "type": "uuid_t", + "description": "A unique process identifier that can be assigned deterministically by multiple system data producers.", + "source": "cpid", + "references": [ + { + "description": "OCSF Common Process Identifier (CPID) Specification", + "url": "https://github.com/ocsf/common-process-id" + } + ], + "requirement": "recommended", + "caption": "Common Process Identifier", + "type_name": "UUID", + "_source": "process_entity" + } + }, + { + "created_time": { + "type": "timestamp_t", + "description": "The time when the process was created/started.", + "requirement": "recommended", + "caption": "Created Time", + "type_name": "Timestamp", + "_source": "process_entity" + } + }, + { + "environment_variables": { + "type": "object_t", + "description": "Environment variables associated with the process.", + "is_array": true, + "requirement": "optional", + "caption": "Environment Variables", + "object_name": "Environment Variable", + "object_type": "environment_variable", + "_source": "process" + } + }, + { + "integrity": { + "type": "string_t", + "description": "The process integrity level, normalized to the caption of the integrity_id value. In the case of 'Other', it is defined by the event source (Windows only).", + "requirement": "optional", + "caption": "Integrity", + "type_name": "String", + "_source": "process", + "_sibling_of": "integrity_id" + } + }, + { + "integrity_id": { + "type": "integer_t", + "enum": { + "3": { + "caption": "Medium" + }, + "6": { + "caption": "Protected" + }, + "0": { + "description": "The integrity level is unknown.", + "caption": "Unknown" + }, + "1": { + "caption": "Untrusted" + }, + "2": { + "caption": "Low" + }, + "99": { + "description": "The integrity level is not mapped. See the integrity attribute, which contains a data source specific value.", + "caption": "Other" + }, + "4": { + "caption": "High" + }, + "5": { + "caption": "System" + } + }, + "description": "The normalized identifier of the process integrity level (Windows only).", + "requirement": "optional", + "caption": "Integrity Level", + "type_name": "Integer", + "sibling": "integrity", + "_source": "process" + } + }, + { + "lineage": { + "type": "file_path_t", + "description": "The lineage of the process, represented by a list of paths for each ancestor process. For example: ['/usr/sbin/sshd', '/usr/bin/bash', '/usr/bin/whoami'].", + "is_array": true, + "requirement": "optional", + "caption": "Lineage", + "type_name": "File Path", + "@deprecated": { + "message": "Use the ancestry attribute.", + "since": "1.4.0" + }, + "_source": "process" + } + }, + { + "namespace_pid": { + "profile": "container", + "type": "integer_t", + "description": "If running under a process namespace (such as in a container), the process identifier within that process namespace.", + "group": "context", + "requirement": "recommended", + "caption": "Namespace PID", + "type_name": "Integer", + "_source": "process" + } + }, + { + "parent_process": { + "type": "object_t", + "description": "The parent process of this process object. It is recommended to only populate this field for the top-level process object, to prevent deep nesting. Additional ancestry information can be supplied in the ancestry attribute.", + "references": [ + { + "description": "Guidance on Representing Process Parentage", + "url": "https://github.com/ocsf/ocsf-docs/blob/main/articles/representing-process-parentage.md" + } + ], + "requirement": "recommended", + "caption": "Parent Process", + "object_name": "Process", + "object_type": "process", + "_source": "process" + } + }, + { + "ptid": { + "type": "long_t", + "description": "The identifier of the process thread associated with the event, as returned by the operating system.", + "requirement": "optional", + "caption": "Process Thread ID", + "type_name": "Long", + "_source": "process" + } + }, + { + "sandbox": { + "type": "string_t", + "description": "The name of the containment jail (i.e., sandbox). For example, hardened_ps, high_security_ps, oracle_ps, netsvcs_ps, or default_ps.", + "requirement": "optional", + "caption": "Sandbox", + "type_name": "String", + "_source": "process" + } + }, + { + "terminated_time": { + "type": "timestamp_t", + "description": "The time when the process was terminated.", + "requirement": "optional", + "caption": "Terminated Time", + "type_name": "Timestamp", + "_source": "process" + } + }, + { + "working_directory": { + "type": "string_t", + "description": "The working directory of a process.", + "requirement": "optional", + "caption": "Working Directory", + "type_name": "String", + "_source": "process" + } + }, + { + "xattributes": { + "type": "object_t", + "description": "An unordered collection of zero or more name/value pairs that represent a process extended attribute.", + "requirement": "optional", + "caption": "Extended Attributes", + "object_name": "Object", + "object_type": "object", + "_source": "process" + } + }, + { + "auid": { + "profile": "linux/linux_users", + "type": "integer_t", + "description": "The audit user assigned at login by the audit subsystem.", + "extension": "linux", + "requirement": "optional", + "extension_id": 1, + "caption": "Audit User ID", + "type_name": "Integer", + "_source": "linux/process", + "_source_patched": "process" + } + }, + { + "egid": { + "profile": "linux/linux_users", + "type": "integer_t", + "description": "The effective group under which this process is running.", + "extension": "linux", + "requirement": "optional", + "extension_id": 1, + "caption": "Effective Group ID", + "type_name": "Integer", + "_source": "linux/process", + "_source_patched": "process" + } + }, + { + "euid": { + "profile": "linux/linux_users", + "type": "integer_t", + "description": "The effective user under which this process is running.", + "extension": "linux", + "requirement": "optional", + "extension_id": 1, + "caption": "Effective User ID", + "type_name": "Integer", + "_source": "linux/process", + "_source_patched": "process" + } + }, + { + "hosted_services": { + "type": "object_t", + "description": "The Windows services that this process is hosting.", + "extension": "win", + "is_array": true, + "requirement": "optional", + "extension_id": 2, + "caption": "Hosted Services", + "object_name": "Windows Service", + "object_type": "win/win_service", + "_source": "win/process", + "_source_patched": "process" + } + }, + { + "terminated_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the process was terminated.", + "requirement": "optional", + "caption": "Terminated Time", + "type_name": "Datetime", + "_source": "process" + } + }, + { + "created_time_dt": { + "profile": "datetime", + "type": "datetime_t", + "description": "The time when the process was created/started.", + "requirement": "optional", + "caption": "Created Time", + "type_name": "Datetime", + "_source": "process_entity" + } + } + ], + "name": "process", + "description": "The Process object describes a running instance of a launched program.", + "extends": "process_entity", + "constraints": { + "at_least_one": [ + "pid", + "uid", + "cpid" + ] + }, + "references": [ + { + "description": "D3FEND\u2122 Ontology d3f:Process", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:Process/" + } + ], + "profiles": [ + "container", + "linux/linux_users", + "data_classification", + "datetime" + ], + "caption": "Process", + "observable": 25 +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/product.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/product.json new file mode 100644 index 00000000..39fe2403 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/product.json @@ -0,0 +1,139 @@ +{ + "attributes": [ + { + "name": { + "type": "string_t", + "description": "The name of the product.", + "requirement": "recommended", + "caption": "Name", + "type_name": "String", + "_source": "product" + } + }, + { + "version": { + "type": "string_t", + "description": "The version of the product, as defined by the event source. For example: 2013.1.3-beta.", + "requirement": "recommended", + "caption": "Version", + "type_name": "String", + "_source": "product" + } + }, + { + "path": { + "type": "string_t", + "description": "The installation path of the product.", + "requirement": "optional", + "caption": "Path", + "type_name": "String", + "_source": "product" + } + }, + { + "uid": { + "type": "string_t", + "description": "The unique identifier of the product.", + "requirement": "recommended", + "caption": "Unique ID", + "type_name": "String", + "_source": "product" + } + }, + { + "feature": { + "type": "object_t", + "description": "The feature that reported the event.", + "requirement": "optional", + "caption": "Feature", + "object_name": "Feature", + "object_type": "feature", + "_source": "product" + } + }, + { + "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", + "_source": "product" + } + }, + { + "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", + "_source": "product" + } + }, + { + "data_classification": { + "profile": "data_classification", + "type": "object_t", + "description": "The Data Classification object includes information about data classification levels and data category types.", + "group": "context", + "requirement": "recommended", + "caption": "Data Classification", + "object_name": "Data Classification", + "object_type": "data_classification", + "@deprecated": { + "message": "Use the attribute data_classifications instead", + "since": "1.4.0" + }, + "_source": "product" + } + }, + { + "data_classifications": { + "profile": "data_classification", + "type": "object_t", + "description": "A list of Data Classification objects, that include information about data classification levels and data category types, identified by a classifier.", + "group": "context", + "is_array": true, + "requirement": "recommended", + "caption": "Data Classification", + "object_name": "Data Classification", + "object_type": "data_classification", + "_source": "product" + } + }, + { + "url_string": { + "type": "url_t", + "description": "The URL pointing towards the product.", + "requirement": "optional", + "caption": "URL String", + "type_name": "URL String", + "_source": "product" + } + }, + { + "vendor_name": { + "type": "string_t", + "description": "The name of the vendor of the product.", + "requirement": "recommended", + "caption": "Vendor Name", + "type_name": "String", + "_source": "product" + } + } + ], + "name": "product", + "description": "The Product object describes characteristics of a software product.", + "extends": "_entity", + "constraints": { + "at_least_one": [ + "name", + "uid" + ] + }, + "profiles": [ + "data_classification" + ], + "caption": "Product" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/remediation.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/remediation.json new file mode 100644 index 00000000..06677c16 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/remediation.json @@ -0,0 +1,74 @@ +{ + "attributes": [ + { + "desc": { + "type": "string_t", + "description": "The description of the remediation strategy.", + "requirement": "required", + "caption": "Description", + "type_name": "String", + "_source": "remediation" + } + }, + { + "references": { + "type": "string_t", + "description": "A list of supporting URL/s, references that help describe the remediation strategy.", + "is_array": true, + "requirement": "optional", + "caption": "References", + "type_name": "String", + "_source": "remediation" + } + }, + { + "cis_controls": { + "type": "object_t", + "description": "An array of Center for Internet Security (CIS) Controls that can be optionally mapped to provide additional remediation details.", + "is_array": true, + "references": [ + { + "description": "Center For Internet Security Controls", + "url": "https://www.cisecurity.org/controls/" + } + ], + "requirement": "optional", + "caption": "CIS Controls", + "object_name": "CIS Control", + "object_type": "cis_control", + "_source": "remediation" + } + }, + { + "kb_article_list": { + "type": "object_t", + "description": "A list of KB articles or patches related to an endpoint. A KB Article contains metadata that describes the patch or an update.", + "is_array": true, + "requirement": "optional", + "caption": "Knowledgebase Articles", + "object_name": "KB Article", + "object_type": "kb_article", + "_source": "remediation" + } + }, + { + "kb_articles": { + "type": "string_t", + "description": "The KB article/s related to the entity. A KB Article contains metadata that describes the patch or an update.", + "is_array": true, + "requirement": "optional", + "caption": "Knowledgebase Articles", + "type_name": "String", + "@deprecated": { + "message": "Use the kb_article_list attribute instead.", + "since": "1.1.0" + }, + "_source": "remediation" + } + } + ], + "name": "remediation", + "description": "The Remediation object describes the recommended remediation steps to address identified issue(s).", + "extends": "object", + "caption": "Remediation" +} diff --git a/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/url.json b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/url.json new file mode 100644 index 00000000..370a8de9 --- /dev/null +++ b/crates/openshell-ocsf/schemas/ocsf/v1.7.0/objects/url.json @@ -0,0 +1,404 @@ +{ + "attributes": [ + { + "port": { + "type": "port_t", + "description": "The URL port. For example: 80.", + "requirement": "recommended", + "caption": "Port", + "type_name": "Port", + "_source": "url" + } + }, + { + "scheme": { + "type": "string_t", + "description": "The scheme portion of the URL. For example: http, https, ftp, or sftp.", + "requirement": "recommended", + "caption": "Scheme", + "type_name": "String", + "_source": "url" + } + }, + { + "path": { + "type": "string_t", + "description": "The URL path as extracted from the URL. For example: /download/trouble from www.example.com/download/trouble.", + "requirement": "recommended", + "caption": "Path", + "type_name": "String", + "_source": "url" + } + }, + { + "domain": { + "type": "string_t", + "description": "The domain portion of the URL. For example: example.com in https://sub.example.com.", + "requirement": "optional", + "caption": "Domain", + "type_name": "String", + "_source": "url" + } + }, + { + "hostname": { + "type": "hostname_t", + "description": "The URL host as extracted from the URL. For example: www.example.com from www.example.com/download/trouble.", + "requirement": "recommended", + "caption": "Hostname", + "type_name": "Hostname", + "_source": "url" + } + }, + { + "query_string": { + "type": "string_t", + "description": "The query portion of the URL. For example: the query portion of the URL http://www.example.com/search?q=bad&sort=date is q=bad&sort=date.", + "requirement": "recommended", + "caption": "HTTP Query String", + "type_name": "String", + "_source": "url" + } + }, + { + "categories": { + "type": "string_t", + "description": "The Website categorization names, as defined by category_ids enum values.", + "is_array": true, + "requirement": "optional", + "caption": "Website Categorization", + "type_name": "String", + "_source": "url", + "_sibling_of": "category_ids" + } + }, + { + "category_ids": { + "type": "integer_t", + "enum": { + "5": { + "caption": "Intimate Apparel/Swimsuit" + }, + "83": { + "caption": "Peer-to-Peer (P2P)" + }, + "35": { + "caption": "Military" + }, + "6": { + "caption": "Nudity" + }, + "93": { + "caption": "Sexual Expression" + }, + "65": { + "caption": "Sports/Recreation" + }, + "64": { + "caption": "Restaurants/Dining/Food" + }, + "11": { + "caption": "Gambling" + }, + "49": { + "caption": "Reference" + }, + "32": { + "caption": "Brokerage/Trading" + }, + "51": { + "caption": "Chat (IM)/SMS" + }, + "27": { + "caption": "Education" + }, + "97": { + "caption": "Content Servers" + }, + "66": { + "caption": "Travel" + }, + "50": { + "caption": "Mixed Content/Potentially Adult" + }, + "96": { + "caption": "Non-Viewable/Infrastructure" + }, + "86": { + "caption": "Proxy Avoidance" + }, + "98": { + "caption": "Placeholders" + }, + "52": { + "caption": "Email" + }, + "25": { + "caption": "Controlled Substances" + }, + "38": { + "caption": "Technology/Internet" + }, + "85": { + "caption": "Office/Business Applications" + }, + "110": { + "caption": "Internet Telephony" + }, + "23": { + "caption": "Alcohol" + }, + "29": { + "caption": "Charitable Organizations" + }, + "118": { + "caption": "Piracy/Copyright Concerns" + }, + "53": { + "caption": "Newsgroups/Forums" + }, + "56": { + "caption": "File Storage/Sharing" + }, + "3": { + "caption": "Pornography" + }, + "36": { + "caption": "Political/Social Advocacy" + }, + "17": { + "caption": "Hacking" + }, + "9": { + "caption": "Scam/Questionable/Illegal" + }, + "63": { + "caption": "Personal Sites" + }, + "89": { + "caption": "Web Hosting" + }, + "112": { + "caption": "Media Sharing" + }, + "121": { + "caption": "Marijuana" + }, + "44": { + "caption": "Malicious Outbound Data/Botnets" + }, + "107": { + "caption": "Informational" + }, + "68": { + "caption": "Humor/Jokes" + }, + "33": { + "caption": "Games" + }, + "92": { + "caption": "Suspicious" + }, + "114": { + "caption": "TV/Video Streams" + }, + "106": { + "caption": "E-Card/Invitations" + }, + "45": { + "caption": "Job Search/Careers" + }, + "108": { + "caption": "Computer/Information Security" + }, + "55": { + "caption": "Social Networking" + }, + "84": { + "caption": "Audio/Video Clips" + }, + "101": { + "caption": "Spam" + }, + "111": { + "caption": "Online Meetings" + }, + "87": { + "caption": "For Kids" + }, + "4": { + "caption": "Sex Education" + }, + "0": { + "description": "The Domain/URL category is unknown.", + "caption": "Unknown" + }, + "15": { + "caption": "Weapons" + }, + "88": { + "caption": "Web Ads/Analytics" + }, + "20": { + "caption": "Entertainment" + }, + "40": { + "caption": "Search Engines/Portals" + }, + "18": { + "caption": "Phishing" + }, + "71": { + "caption": "Software Downloads" + }, + "61": { + "caption": "Society/Daily Living" + }, + "30": { + "caption": "Art/Culture" + }, + "22": { + "caption": "Alternative Spirituality/Belief" + }, + "47": { + "caption": "Personals/Dating" + }, + "14": { + "caption": "Violence/Hate/Racism" + }, + "59": { + "caption": "Auctions" + }, + "58": { + "caption": "Shopping" + }, + "60": { + "caption": "Real Estate" + }, + "95": { + "caption": "Translation" + }, + "113": { + "caption": "Radio/Audio Streams" + }, + "16": { + "caption": "Abortion" + }, + "54": { + "caption": "Religion" + }, + "46": { + "caption": "News/Media" + }, + "67": { + "caption": "Vehicles" + }, + "103": { + "caption": "Dynamic DNS Host" + }, + "24": { + "caption": "Tobacco" + }, + "57": { + "caption": "Remote Access Tools" + }, + "109": { + "caption": "Internet Connected Devices" + }, + "102": { + "caption": "Potentially Unwanted Software" + }, + "7": { + "caption": "Extreme" + }, + "99": { + "description": "The Domain/URL category is not mapped. See the categories attribute, which contains a data source specific value.", + "caption": "Other" + }, + "34": { + "caption": "Government/Legal" + }, + "26": { + "caption": "Child Pornography" + }, + "43": { + "caption": "Malicious Sources/Malnets" + }, + "90": { + "caption": "Uncategorized" + }, + "37": { + "caption": "Health" + }, + "31": { + "caption": "Financial Services" + }, + "21": { + "caption": "Business/Economy" + }, + "1": { + "caption": "Adult/Mature Content" + } + }, + "description": "The Website categorization identifiers.", + "is_array": true, + "requirement": "recommended", + "caption": "Website Categorization IDs", + "type_name": "Integer", + "sibling": "categories", + "_source": "url" + } + }, + { + "resource_type": { + "type": "string_t", + "description": "The context in which a resource was retrieved in a web request.", + "requirement": "optional", + "caption": "Resource Type", + "type_name": "String", + "_source": "url" + } + }, + { + "subdomain": { + "type": "string_t", + "description": "The subdomain portion of the URL. For example: sub in https://sub.example.com or sub2.sub1 in https://sub2.sub1.example.com.", + "requirement": "optional", + "caption": "Subdomain", + "type_name": "String", + "_source": "url" + } + }, + { + "url_string": { + "type": "url_t", + "description": "The URL string. See RFC 1738. For example: http://www.example.com/download/trouble.exe. Note: The URL path should not populate the URL string.", + "requirement": "recommended", + "caption": "URL String", + "type_name": "URL String", + "_source": "url" + } + } + ], + "name": "url", + "description": "The Uniform Resource Locator (URL) object describes the characteristics of a URL.", + "extends": "object", + "constraints": { + "at_least_one": [ + "url_string", + "path" + ] + }, + "references": [ + { + "description": "Defined in RFC 1738", + "url": "https://datatracker.ietf.org/doc/html/rfc1738" + }, + { + "description": "D3FEND\u2122 Ontology d3f:URL", + "url": "https://d3fend.mitre.org/dao/artifact/d3f:URL/" + } + ], + "caption": "Uniform Resource Locator", + "observable": 23 +} diff --git a/crates/openshell-ocsf/src/builders/base.rs b/crates/openshell-ocsf/src/builders/base.rs new file mode 100644 index 00000000..5141bff9 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/base.rs @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for Base Event [0]. + +use crate::builders::SandboxContext; +use crate::enums::{SeverityId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{BaseEvent, OcsfEvent}; + +/// Builder for Base Event [0] — events without a specific OCSF class. +pub struct BaseEventBuilder<'a> { + ctx: &'a SandboxContext, + severity: SeverityId, + status: Option, + message: Option, + activity_name: Option, + unmapped: serde_json::Map, +} + +impl<'a> BaseEventBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + severity: SeverityId::Informational, + status: None, + message: None, + activity_name: None, + unmapped: serde_json::Map::new(), + } + } + + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + #[must_use] + pub fn activity_name(mut self, name: impl Into) -> Self { + self.activity_name = Some(name.into()); + self + } + + /// Add an unmapped field. + #[must_use] + pub fn unmapped(mut self, key: &str, value: impl Into) -> Self { + self.unmapped.insert(key.to_string(), value.into()); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self.activity_name.as_deref().unwrap_or("Other"); + let mut base = BaseEventData::new( + 0, + "Base Event", + 0, + "Uncategorized", + 99, + activity_name, + self.severity, + self.ctx.metadata(&["container", "host"]), + ); + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + if !self.unmapped.is_empty() { + base.unmapped = Some(serde_json::Value::Object(self.unmapped)); + } + + OcsfEvent::Base(BaseEvent { base }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_base_event_builder() { + let ctx = test_sandbox_context(); + let event = BaseEventBuilder::new(&ctx) + .severity(SeverityId::Informational) + .status(StatusId::Success) + .activity_name("Network Namespace Created") + .message("Network namespace created") + .unmapped("namespace", serde_json::json!("openshell-sandbox-abc123")) + .unmapped("host_ip", serde_json::json!("10.42.0.1")) + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 0); + assert_eq!(json["activity_name"], "Network Namespace Created"); + assert_eq!(json["message"], "Network namespace created"); + assert_eq!(json["unmapped"]["namespace"], "openshell-sandbox-abc123"); + } +} diff --git a/crates/openshell-ocsf/src/builders/config.rs b/crates/openshell-ocsf/src/builders/config.rs new file mode 100644 index 00000000..05d62626 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/config.rs @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for Device Config State Change [5019] events. + +use crate::builders::SandboxContext; +use crate::enums::{SecurityLevelId, SeverityId, StateId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{DeviceConfigStateChangeEvent, OcsfEvent}; + +/// Builder for Device Config State Change [5019] events. +pub struct ConfigStateChangeBuilder<'a> { + ctx: &'a SandboxContext, + severity: SeverityId, + status: Option, + state_id: Option, + state_label: Option, + security_level: Option, + prev_security_level: Option, + message: Option, + unmapped: serde_json::Map, +} + +impl<'a> ConfigStateChangeBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + severity: SeverityId::Informational, + status: None, + state_id: None, + state_label: None, + security_level: None, + prev_security_level: None, + message: None, + unmapped: serde_json::Map::new(), + } + } + + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + + /// Set state with a custom label (OCSF `state_id` + display label). + #[must_use] + pub fn state(mut self, id: StateId, label: &str) -> Self { + self.state_id = Some(id); + self.state_label = Some(label.to_string()); + self + } + + #[must_use] + pub fn security_level(mut self, id: SecurityLevelId) -> Self { + self.security_level = Some(id); + self + } + #[must_use] + pub fn prev_security_level(mut self, id: SecurityLevelId) -> Self { + self.prev_security_level = Some(id); + self + } + + /// Add an unmapped field. + #[must_use] + pub fn unmapped(mut self, key: &str, value: impl Into) -> Self { + self.unmapped.insert(key.to_string(), value.into()); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let mut base = BaseEventData::new( + 5019, + "Device Config State Change", + 5, + "Discovery", + 1, + "Log", // activity_id=1 (Log) + self.severity, + self.ctx + .metadata(&["security_control", "container", "host"]), + ); + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + if !self.unmapped.is_empty() { + base.unmapped = Some(serde_json::Value::Object(self.unmapped)); + } + + OcsfEvent::DeviceConfigStateChange(DeviceConfigStateChangeEvent { + base, + state_id: self.state_id.map(StateId::as_u8), + state: self.state_label, + security_level_id: self.security_level.map(SecurityLevelId::as_u8), + security_level: self.security_level.map(|s| s.label().to_string()), + prev_security_level_id: self.prev_security_level.map(SecurityLevelId::as_u8), + prev_security_level: self.prev_security_level.map(|s| s.label().to_string()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_config_state_change_builder() { + let ctx = test_sandbox_context(); + let event = ConfigStateChangeBuilder::new(&ctx) + .state(StateId::Enabled, "loaded") + .security_level(SecurityLevelId::Secure) + .prev_security_level(SecurityLevelId::Unknown) + .severity(SeverityId::Informational) + .status(StatusId::Success) + .unmapped("policy_version", serde_json::json!("v3")) + .unmapped("policy_hash", serde_json::json!("sha256:abc123")) + .message("Policy reloaded successfully") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 5019); + assert_eq!(json["state_id"], 2); + assert_eq!(json["security_level"], "Secure"); + assert_eq!(json["unmapped"]["policy_version"], "v3"); + } +} diff --git a/crates/openshell-ocsf/src/builders/finding.rs b/crates/openshell-ocsf/src/builders/finding.rs new file mode 100644 index 00000000..1d38f822 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/finding.rs @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for Detection Finding [2004] events. + +use crate::builders::SandboxContext; +use crate::enums::{ActionId, ActivityId, ConfidenceId, DispositionId, RiskLevelId, SeverityId}; +use crate::events::base_event::BaseEventData; +use crate::events::{DetectionFindingEvent, OcsfEvent}; +use crate::objects::{Attack, Evidence, FindingInfo, Remediation}; + +/// Builder for Detection Finding [2004] events. +pub struct DetectionFindingBuilder<'a> { + ctx: &'a SandboxContext, + activity: ActivityId, + severity: SeverityId, + action: Option, + disposition: Option, + finding_info: Option, + evidences: Vec, + attacks: Vec, + remediation: Option, + is_alert: Option, + confidence: Option, + risk_level: Option, + message: Option, + log_source: Option, +} + +impl<'a> DetectionFindingBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + activity: ActivityId::Open, + severity: SeverityId::Medium, + action: None, + disposition: None, + finding_info: None, + evidences: Vec::new(), + attacks: Vec::new(), + remediation: None, + is_alert: None, + confidence: None, + risk_level: None, + message: None, + log_source: None, + } + } + + #[must_use] + pub fn activity(mut self, id: ActivityId) -> Self { + self.activity = id; + self + } + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn action(mut self, id: ActionId) -> Self { + self.action = Some(id); + self + } + #[must_use] + pub fn disposition(mut self, id: DispositionId) -> Self { + self.disposition = Some(id); + self + } + #[must_use] + pub fn finding_info(mut self, info: FindingInfo) -> Self { + self.finding_info = Some(info); + self + } + #[must_use] + pub fn is_alert(mut self, alert: bool) -> Self { + self.is_alert = Some(alert); + self + } + #[must_use] + pub fn confidence(mut self, id: ConfidenceId) -> Self { + self.confidence = Some(id); + self + } + #[must_use] + pub fn risk_level(mut self, id: RiskLevelId) -> Self { + self.risk_level = Some(id); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + #[must_use] + pub fn log_source(mut self, source: impl Into) -> Self { + self.log_source = Some(source.into()); + self + } + + /// Add a remediation description. + #[must_use] + pub fn remediation(mut self, desc: &str) -> Self { + self.remediation = Some(Remediation::new(desc)); + self + } + + /// Add evidence key-value pair. + #[must_use] + pub fn evidence(mut self, key: &str, value: &str) -> Self { + self.evidences.push(Evidence::from_pairs(&[(key, value)])); + self + } + + /// Add evidence from multiple pairs. + #[must_use] + pub fn evidence_pairs(mut self, pairs: &[(&str, &str)]) -> Self { + self.evidences.push(Evidence::from_pairs(pairs)); + self + } + + /// Add a MITRE ATT&CK mapping. + #[must_use] + pub fn attack(mut self, attack: Attack) -> Self { + self.attacks.push(attack); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self.activity.finding_label().to_string(); + let mut metadata = self + .ctx + .metadata(&["security_control", "container", "host"]); + if let Some(source) = self.log_source { + metadata.log_source = Some(source); + } + + let mut base = BaseEventData::new( + 2004, + "Detection Finding", + 2, + "Findings", + self.activity.as_u8(), + &activity_name, + self.severity, + metadata, + ); + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + + OcsfEvent::DetectionFinding(DetectionFindingEvent { + base, + finding_info: self + .finding_info + .unwrap_or_else(|| FindingInfo::new("unknown", "Unknown Finding")), + evidences: if self.evidences.is_empty() { + None + } else { + Some(self.evidences) + }, + attacks: if self.attacks.is_empty() { + None + } else { + Some(self.attacks) + }, + remediation: self.remediation, + is_alert: self.is_alert, + confidence_id: self.confidence.map(ConfidenceId::as_u8), + confidence: self.confidence.map(|c| c.label().to_string()), + risk_level_id: self.risk_level.map(RiskLevelId::as_u8), + risk_level: self.risk_level.map(|r| r.label().to_string()), + action_id: self.action.map(ActionId::as_u8), + action: self.action.map(|a| a.label().to_string()), + disposition_id: self.disposition.map(DispositionId::as_u8), + disposition: self.disposition.map(|d| d.label().to_string()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_detection_finding_builder() { + let ctx = test_sandbox_context(); + let event = DetectionFindingBuilder::new(&ctx) + .activity(ActivityId::Open) // Create + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .is_alert(true) + .confidence(ConfidenceId::High) + .risk_level(RiskLevelId::High) + .finding_info( + FindingInfo::new("nssh1-replay-abc", "NSSH1 Nonce Replay Attack") + .with_desc("A nonce was replayed."), + ) + .evidence("nonce", "0xdeadbeef") + .attack(Attack::mitre( + "T1550", + "Use Alternate Authentication Material", + "TA0008", + "Lateral Movement", + )) + .message("NSSH1 nonce replay detected") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 2004); + assert_eq!(json["finding_info"]["title"], "NSSH1 Nonce Replay Attack"); + assert_eq!(json["is_alert"], true); + assert_eq!(json["confidence"], "High"); + } +} diff --git a/crates/openshell-ocsf/src/builders/http.rs b/crates/openshell-ocsf/src/builders/http.rs new file mode 100644 index 00000000..449c2f66 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/http.rs @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for HTTP Activity [4002] events. + +use crate::builders::SandboxContext; +use crate::enums::{ActionId, ActivityId, DispositionId, SeverityId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{HttpActivityEvent, OcsfEvent}; +use crate::objects::{Actor, Endpoint, FirewallRule, HttpRequest, HttpResponse, Process}; + +/// Builder for HTTP Activity [4002] events. +pub struct HttpActivityBuilder<'a> { + ctx: &'a SandboxContext, + activity: ActivityId, + action: Option, + disposition: Option, + severity: SeverityId, + status: Option, + http_request: Option, + http_response: Option, + src_endpoint: Option, + dst_endpoint: Option, + actor: Option, + firewall_rule: Option, + message: Option, +} + +impl<'a> HttpActivityBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + activity: ActivityId::Unknown, + action: None, + disposition: None, + severity: SeverityId::Informational, + status: None, + http_request: None, + http_response: None, + src_endpoint: None, + dst_endpoint: None, + actor: None, + firewall_rule: None, + message: None, + } + } + + #[must_use] + pub fn activity(mut self, id: ActivityId) -> Self { + self.activity = id; + self + } + #[must_use] + pub fn action(mut self, id: ActionId) -> Self { + self.action = Some(id); + self + } + #[must_use] + pub fn disposition(mut self, id: DispositionId) -> Self { + self.disposition = Some(id); + self + } + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn http_request(mut self, req: HttpRequest) -> Self { + self.http_request = Some(req); + self + } + #[must_use] + pub fn http_response(mut self, resp: HttpResponse) -> Self { + self.http_response = Some(resp); + self + } + #[must_use] + pub fn src_endpoint(mut self, ep: Endpoint) -> Self { + self.src_endpoint = Some(ep); + self + } + #[must_use] + pub fn dst_endpoint(mut self, ep: Endpoint) -> Self { + self.dst_endpoint = Some(ep); + self + } + #[must_use] + pub fn actor_process(mut self, process: Process) -> Self { + self.actor = Some(Actor { process }); + self + } + #[must_use] + pub fn firewall_rule(mut self, name: &str, rule_type: &str) -> Self { + self.firewall_rule = Some(FirewallRule::new(name, rule_type)); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self.activity.http_label().to_string(); + let mut base = BaseEventData::new( + 4002, + "HTTP Activity", + 4, + "Network Activity", + self.activity.as_u8(), + &activity_name, + self.severity, + self.ctx + .metadata(&["security_control", "network_proxy", "container", "host"]), + ); + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + + OcsfEvent::HttpActivity(HttpActivityEvent { + base, + http_request: self.http_request, + http_response: self.http_response, + src_endpoint: self.src_endpoint, + dst_endpoint: self.dst_endpoint, + proxy_endpoint: Some(self.ctx.proxy_endpoint()), + actor: self.actor, + firewall_rule: self.firewall_rule, + action_id: self.action.map(ActionId::as_u8), + action: self.action.map(|a| a.label().to_string()), + disposition_id: self.disposition.map(DispositionId::as_u8), + disposition: self.disposition.map(|d| d.label().to_string()), + observation_point_id: Some(2), + is_src_dst_assignment_known: Some(true), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + use crate::objects::Url; + + #[test] + fn test_http_activity_builder() { + let ctx = test_sandbox_context(); + let event = HttpActivityBuilder::new(&ctx) + .activity(ActivityId::Reset) // Get = 3 + .action(ActionId::Allowed) + .disposition(DispositionId::Allowed) + .severity(SeverityId::Informational) + .http_request(HttpRequest::new( + "GET", + Url::new("https", "api.example.com", "/v1/data", 443), + )) + .actor_process(Process::new("curl", 88)) + .firewall_rule("default-egress", "mechanistic") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 4002); + assert_eq!(json["activity_name"], "Get"); + assert_eq!(json["http_request"]["http_method"], "GET"); + assert_eq!(json["actor"]["process"]["name"], "curl"); + } +} diff --git a/crates/openshell-ocsf/src/builders/lifecycle.rs b/crates/openshell-ocsf/src/builders/lifecycle.rs new file mode 100644 index 00000000..b1f42d19 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/lifecycle.rs @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for Application Lifecycle [6002] events. + +use crate::builders::SandboxContext; +use crate::enums::{ActivityId, SeverityId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{ApplicationLifecycleEvent, OcsfEvent}; +use crate::objects::Product; + +/// Builder for Application Lifecycle [6002] events. +pub struct AppLifecycleBuilder<'a> { + ctx: &'a SandboxContext, + activity: ActivityId, + severity: SeverityId, + status: Option, + message: Option, +} + +impl<'a> AppLifecycleBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + activity: ActivityId::Unknown, + severity: SeverityId::Informational, + status: None, + message: None, + } + } + + #[must_use] + pub fn activity(mut self, id: ActivityId) -> Self { + self.activity = id; + self + } + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self.activity.lifecycle_label().to_string(); + let mut base = BaseEventData::new( + 6002, + "Application Lifecycle", + 6, + "Application Activity", + self.activity.as_u8(), + &activity_name, + self.severity, + self.ctx.metadata(&["container", "host"]), + ); + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + + OcsfEvent::ApplicationLifecycle(ApplicationLifecycleEvent { + base, + app: Product::openshell_sandbox(&self.ctx.product_version), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_app_lifecycle_builder() { + let ctx = test_sandbox_context(); + let event = AppLifecycleBuilder::new(&ctx) + .activity(ActivityId::Reset) // Start + .severity(SeverityId::Informational) + .status(StatusId::Success) + .message("Starting sandbox") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 6002); + assert_eq!(json["activity_name"], "Start"); + assert_eq!(json["app"]["name"], "OpenShell Sandbox Supervisor"); + assert_eq!(json["status"], "Success"); + } +} diff --git a/crates/openshell-ocsf/src/builders/mod.rs b/crates/openshell-ocsf/src/builders/mod.rs new file mode 100644 index 00000000..77004da4 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/mod.rs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Ergonomic builders for constructing OCSF events. +//! +//! Each event class has a builder that takes a `SandboxContext` reference +//! and provides chainable methods for setting event fields. + +mod base; +mod config; +mod finding; +mod http; +mod lifecycle; +mod network; +mod process; +mod ssh; + +pub use base::BaseEventBuilder; +pub use config::ConfigStateChangeBuilder; +pub use finding::DetectionFindingBuilder; +pub use http::HttpActivityBuilder; +pub use lifecycle::AppLifecycleBuilder; +pub use network::NetworkActivityBuilder; +pub use process::ProcessActivityBuilder; +pub use ssh::SshActivityBuilder; + +use std::net::IpAddr; + +use crate::OCSF_VERSION; +use crate::objects::{Container, Device, Endpoint, Image, Metadata, Product}; + +/// Immutable context created once at sandbox startup. +/// +/// Passed to every event builder to populate shared OCSF fields +/// (metadata, container, device, proxy endpoint). +#[derive(Debug, Clone)] +pub struct SandboxContext { + /// Sandbox unique identifier. + pub sandbox_id: String, + /// Sandbox display name. + pub sandbox_name: String, + /// Container image reference. + pub container_image: String, + /// Device hostname. + pub hostname: String, + /// Product version string. + pub product_version: String, + /// Proxy listen IP address. + pub proxy_ip: IpAddr, + /// Proxy listen port. + pub proxy_port: u16, +} + +impl SandboxContext { + /// Build the OCSF `Metadata` object for any event. + #[must_use] + pub fn metadata(&self, profiles: &[&str]) -> Metadata { + Metadata { + version: OCSF_VERSION.to_string(), + product: Product::openshell_sandbox(&self.product_version), + profiles: profiles.iter().map(|s| (*s).to_string()).collect(), + uid: Some(self.sandbox_id.clone()), + log_source: None, + } + } + + /// Build the OCSF `Container` object. + #[must_use] + pub fn container(&self) -> Container { + Container { + name: self.sandbox_name.clone(), + uid: Some(self.sandbox_id.clone()), + image: Some(Image { + name: self.container_image.clone(), + }), + } + } + + /// Build the OCSF `Device` object. + #[must_use] + pub fn device(&self) -> Device { + Device::linux(&self.hostname) + } + + /// Build the `proxy_endpoint` object for the Network Proxy profile. + #[must_use] + pub fn proxy_endpoint(&self) -> Endpoint { + Endpoint::from_ip(self.proxy_ip, self.proxy_port) + } +} + +#[cfg(test)] +pub(crate) fn test_sandbox_context() -> SandboxContext { + SandboxContext { + sandbox_id: "sandbox-abc123".to_string(), + sandbox_name: "my-sandbox".to_string(), + container_image: "ghcr.io/openshell/sandbox:latest".to_string(), + hostname: "sandbox-abc123".to_string(), + product_version: "0.1.0".to_string(), + proxy_ip: "10.42.0.1".parse().unwrap(), + proxy_port: 3128, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sandbox_context_metadata() { + let ctx = test_sandbox_context(); + let meta = ctx.metadata(&["security_control", "container"]); + assert_eq!(meta.version, "1.7.0"); + assert_eq!(meta.product.name, "OpenShell Sandbox Supervisor"); + assert_eq!(meta.profiles.len(), 2); + assert_eq!(meta.uid.as_deref(), Some("sandbox-abc123")); + } + + #[test] + fn test_sandbox_context_container() { + let ctx = test_sandbox_context(); + let container = ctx.container(); + assert_eq!(container.name, "my-sandbox"); + assert_eq!(container.uid.as_deref(), Some("sandbox-abc123")); + } + + #[test] + fn test_sandbox_context_device() { + let ctx = test_sandbox_context(); + let device = ctx.device(); + assert_eq!(device.hostname, "sandbox-abc123"); + } + + #[test] + fn test_sandbox_context_proxy_endpoint() { + let ctx = test_sandbox_context(); + let ep = ctx.proxy_endpoint(); + assert_eq!(ep.ip.as_deref(), Some("10.42.0.1")); + assert_eq!(ep.port, Some(3128)); + } +} diff --git a/crates/openshell-ocsf/src/builders/network.rs b/crates/openshell-ocsf/src/builders/network.rs new file mode 100644 index 00000000..f0f84e50 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/network.rs @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for Network Activity [4001] events. + +use std::net::IpAddr; + +use crate::builders::SandboxContext; +use crate::enums::{ActionId, ActivityId, DispositionId, SeverityId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{NetworkActivityEvent, OcsfEvent}; +use crate::objects::{Actor, ConnectionInfo, Endpoint, FirewallRule, Process}; + +/// Builder for Network Activity [4001] events. +pub struct NetworkActivityBuilder<'a> { + ctx: &'a SandboxContext, + activity: ActivityId, + activity_name: Option, + action: Option, + disposition: Option, + severity: SeverityId, + status: Option, + src_endpoint: Option, + dst_endpoint: Option, + actor: Option, + firewall_rule: Option, + connection_info: Option, + observation_point_id: Option, + message: Option, + status_detail: Option, + unmapped: Option>, + log_source: Option, +} + +impl<'a> NetworkActivityBuilder<'a> { + /// Start building a Network Activity event. + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + activity: ActivityId::Unknown, + activity_name: None, + action: None, + disposition: None, + severity: SeverityId::Informational, + status: None, + src_endpoint: None, + dst_endpoint: None, + actor: None, + firewall_rule: None, + connection_info: None, + observation_point_id: None, + message: None, + status_detail: None, + unmapped: None, + log_source: None, + } + } + + #[must_use] + pub fn activity(mut self, id: ActivityId) -> Self { + self.activity = id; + self + } + #[must_use] + pub fn activity_name(mut self, name: impl Into) -> Self { + self.activity_name = Some(name.into()); + self + } + #[must_use] + pub fn action(mut self, id: ActionId) -> Self { + self.action = Some(id); + self + } + #[must_use] + pub fn disposition(mut self, id: DispositionId) -> Self { + self.disposition = Some(id); + self + } + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn src_endpoint_addr(mut self, ip: IpAddr, port: u16) -> Self { + self.src_endpoint = Some(Endpoint::from_ip(ip, port)); + self + } + #[must_use] + pub fn dst_endpoint(mut self, endpoint: Endpoint) -> Self { + self.dst_endpoint = Some(endpoint); + self + } + #[must_use] + pub fn actor_process(mut self, process: Process) -> Self { + self.actor = Some(Actor { process }); + self + } + #[must_use] + pub fn firewall_rule(mut self, name: &str, rule_type: &str) -> Self { + self.firewall_rule = Some(FirewallRule::new(name, rule_type)); + self + } + #[must_use] + pub fn connection_info(mut self, info: ConnectionInfo) -> Self { + self.connection_info = Some(info); + self + } + #[must_use] + pub fn observation_point(mut self, id: u8) -> Self { + self.observation_point_id = Some(id); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + #[must_use] + pub fn status_detail(mut self, detail: impl Into) -> Self { + self.status_detail = Some(detail.into()); + self + } + #[must_use] + pub fn log_source(mut self, source: impl Into) -> Self { + self.log_source = Some(source.into()); + self + } + + /// Add an unmapped field. + #[must_use] + pub fn unmapped(mut self, key: &str, value: impl Into) -> Self { + self.unmapped + .get_or_insert_with(serde_json::Map::new) + .insert(key.to_string(), value.into()); + self + } + + /// Finalize and return the `OcsfEvent`. + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self + .activity_name + .unwrap_or_else(|| self.activity.network_label().to_string()); + let mut metadata = + self.ctx + .metadata(&["security_control", "network_proxy", "container", "host"]); + if let Some(source) = self.log_source { + metadata.log_source = Some(source); + } + + let mut base = BaseEventData::new( + 4001, + "Network Activity", + 4, + "Network Activity", + self.activity.as_u8(), + &activity_name, + self.severity, + metadata, + ); + + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + if let Some(detail) = self.status_detail { + base.set_status_detail(detail); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + if let Some(unmapped) = self.unmapped { + base.unmapped = Some(serde_json::Value::Object(unmapped)); + } + + OcsfEvent::NetworkActivity(NetworkActivityEvent { + base, + src_endpoint: self.src_endpoint, + dst_endpoint: self.dst_endpoint, + proxy_endpoint: Some(self.ctx.proxy_endpoint()), + actor: self.actor, + firewall_rule: self.firewall_rule, + connection_info: self.connection_info, + action_id: self.action.map(ActionId::as_u8), + action: self.action.map(|a| a.label().to_string()), + disposition_id: self.disposition.map(DispositionId::as_u8), + disposition: self.disposition.map(|d| d.label().to_string()), + observation_point_id: self.observation_point_id, + is_src_dst_assignment_known: Some(true), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_network_activity_builder() { + let ctx = test_sandbox_context(); + let event = NetworkActivityBuilder::new(&ctx) + .activity(ActivityId::Open) + .action(ActionId::Allowed) + .disposition(DispositionId::Allowed) + .severity(SeverityId::Informational) + .status(StatusId::Success) + .dst_endpoint(Endpoint::from_domain("api.example.com", 443)) + .actor_process(Process::new("python3", 42).with_cmd_line("python3 /app/main.py")) + .firewall_rule("default-egress", "mechanistic") + .observation_point(2) + .message("CONNECT api.example.com:443 allowed") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 4001); + assert_eq!(json["activity_name"], "Open"); + assert_eq!(json["action"], "Allowed"); + assert_eq!(json["disposition"], "Allowed"); + 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"]["hostname"], "sandbox-abc123"); + assert_eq!(json["is_src_dst_assignment_known"], true); + } +} diff --git a/crates/openshell-ocsf/src/builders/process.rs b/crates/openshell-ocsf/src/builders/process.rs new file mode 100644 index 00000000..016816bf --- /dev/null +++ b/crates/openshell-ocsf/src/builders/process.rs @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for Process Activity [1007] events. + +use crate::builders::SandboxContext; +use crate::enums::{ActionId, ActivityId, DispositionId, LaunchTypeId, SeverityId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{OcsfEvent, ProcessActivityEvent}; +use crate::objects::{Actor, Process}; + +/// Builder for Process Activity [1007] events. +pub struct ProcessActivityBuilder<'a> { + ctx: &'a SandboxContext, + activity: ActivityId, + severity: SeverityId, + status: Option, + action: Option, + disposition: Option, + process: Option, + actor: Option, + launch_type: Option, + exit_code: Option, + message: Option, +} + +impl<'a> ProcessActivityBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + activity: ActivityId::Unknown, + severity: SeverityId::Informational, + status: None, + action: None, + disposition: None, + process: None, + actor: None, + launch_type: None, + exit_code: None, + message: None, + } + } + + #[must_use] + pub fn activity(mut self, id: ActivityId) -> Self { + self.activity = id; + self + } + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn action(mut self, id: ActionId) -> Self { + self.action = Some(id); + self + } + #[must_use] + pub fn disposition(mut self, id: DispositionId) -> Self { + self.disposition = Some(id); + self + } + #[must_use] + pub fn process(mut self, proc: Process) -> Self { + self.process = Some(proc); + self + } + #[must_use] + pub fn actor_process(mut self, process: Process) -> Self { + self.actor = Some(Actor { process }); + self + } + #[must_use] + pub fn launch_type(mut self, lt: LaunchTypeId) -> Self { + self.launch_type = Some(lt); + self + } + #[must_use] + pub fn exit_code(mut self, code: i32) -> Self { + self.exit_code = Some(code); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self.activity.process_label().to_string(); + let mut base = BaseEventData::new( + 1007, + "Process Activity", + 1, + "System Activity", + self.activity.as_u8(), + &activity_name, + self.severity, + self.ctx + .metadata(&["security_control", "container", "host"]), + ); + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + + OcsfEvent::ProcessActivity(ProcessActivityEvent { + base, + process: self.process.unwrap_or_else(|| Process::new("unknown", 0)), + actor: self.actor, + launch_type_id: self.launch_type.map(LaunchTypeId::as_u8), + launch_type: self.launch_type.map(|lt| lt.label().to_string()), + exit_code: self.exit_code, + action_id: self.action.map(ActionId::as_u8), + action: self.action.map(|a| a.label().to_string()), + disposition_id: self.disposition.map(DispositionId::as_u8), + disposition: self.disposition.map(|d| d.label().to_string()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_process_activity_builder_launch() { + let ctx = test_sandbox_context(); + let event = ProcessActivityBuilder::new(&ctx) + .activity(ActivityId::Open) // Launch + .action(ActionId::Allowed) + .disposition(DispositionId::Allowed) + .severity(SeverityId::Informational) + .launch_type(LaunchTypeId::Spawn) + .process(Process::new("python3", 42).with_cmd_line("python3 /app/main.py")) + .actor_process(Process::new("openshell-sandbox", 1)) + .message("Process started: python3 /app/main.py") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 1007); + assert_eq!(json["launch_type"], "Spawn"); + assert_eq!(json["process"]["name"], "python3"); + assert_eq!(json["actor"]["process"]["name"], "openshell-sandbox"); + } + + #[test] + fn test_process_activity_builder_terminate() { + let ctx = test_sandbox_context(); + let event = ProcessActivityBuilder::new(&ctx) + .activity(ActivityId::Close) // Terminate + .severity(SeverityId::Informational) + .process(Process::new("python3", 42)) + .exit_code(0) + .build(); + + let json = event.to_json(); + assert_eq!(json["exit_code"], 0); + } +} diff --git a/crates/openshell-ocsf/src/builders/ssh.rs b/crates/openshell-ocsf/src/builders/ssh.rs new file mode 100644 index 00000000..efa28d40 --- /dev/null +++ b/crates/openshell-ocsf/src/builders/ssh.rs @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for SSH Activity [4007] events. + +use std::net::IpAddr; + +use crate::builders::SandboxContext; +use crate::enums::{ActionId, ActivityId, AuthTypeId, DispositionId, SeverityId, StatusId}; +use crate::events::base_event::BaseEventData; +use crate::events::{OcsfEvent, SshActivityEvent}; +use crate::objects::{Actor, Endpoint, Process}; + +/// Builder for SSH Activity [4007] events. +pub struct SshActivityBuilder<'a> { + ctx: &'a SandboxContext, + activity: ActivityId, + action: Option, + disposition: Option, + severity: SeverityId, + status: Option, + src_endpoint: Option, + dst_endpoint: Option, + actor: Option, + auth_type_id: Option, + auth_type_label: Option, + protocol_ver: Option, + message: Option, +} + +impl<'a> SshActivityBuilder<'a> { + #[must_use] + pub fn new(ctx: &'a SandboxContext) -> Self { + Self { + ctx, + activity: ActivityId::Unknown, + action: None, + disposition: None, + severity: SeverityId::Informational, + status: None, + src_endpoint: None, + dst_endpoint: None, + actor: None, + auth_type_id: None, + auth_type_label: None, + protocol_ver: None, + message: None, + } + } + + #[must_use] + pub fn activity(mut self, id: ActivityId) -> Self { + self.activity = id; + self + } + #[must_use] + pub fn action(mut self, id: ActionId) -> Self { + self.action = Some(id); + self + } + #[must_use] + pub fn disposition(mut self, id: DispositionId) -> Self { + self.disposition = Some(id); + self + } + #[must_use] + pub fn severity(mut self, id: SeverityId) -> Self { + self.severity = id; + self + } + #[must_use] + pub fn status(mut self, id: StatusId) -> Self { + self.status = Some(id); + self + } + #[must_use] + pub fn src_endpoint_addr(mut self, ip: IpAddr, port: u16) -> Self { + self.src_endpoint = Some(Endpoint::from_ip(ip, port)); + self + } + #[must_use] + pub fn dst_endpoint(mut self, ep: Endpoint) -> Self { + self.dst_endpoint = Some(ep); + self + } + #[must_use] + pub fn actor_process(mut self, process: Process) -> Self { + self.actor = Some(Actor { process }); + self + } + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + + /// Set auth type with a custom label (e.g., "NSSH1"). + #[must_use] + pub fn auth_type(mut self, id: AuthTypeId, label: &str) -> Self { + self.auth_type_id = Some(id); + self.auth_type_label = Some(label.to_string()); + self + } + + #[must_use] + pub fn protocol_ver(mut self, ver: &str) -> Self { + self.protocol_ver = Some(ver.to_string()); + self + } + + #[must_use] + pub fn build(self) -> OcsfEvent { + let activity_name = self.activity.network_label().to_string(); + let mut base = BaseEventData::new( + 4007, + "SSH Activity", + 4, + "Network Activity", + self.activity.as_u8(), + &activity_name, + self.severity, + self.ctx + .metadata(&["security_control", "container", "host"]), + ); + if let Some(status) = self.status { + base.set_status(status); + } + if let Some(msg) = self.message { + base.set_message(msg); + } + base.set_device(self.ctx.device()); + base.set_container(self.ctx.container()); + + OcsfEvent::SshActivity(SshActivityEvent { + base, + src_endpoint: self.src_endpoint, + dst_endpoint: self.dst_endpoint, + actor: self.actor, + auth_type_id: self.auth_type_id.map(AuthTypeId::as_u8), + auth_type: self.auth_type_label, + protocol_ver: self.protocol_ver, + action_id: self.action.map(ActionId::as_u8), + action: self.action.map(|a| a.label().to_string()), + disposition_id: self.disposition.map(DispositionId::as_u8), + disposition: self.disposition.map(|d| d.label().to_string()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::test_sandbox_context; + + #[test] + fn test_ssh_activity_builder() { + let ctx = test_sandbox_context(); + let event = SshActivityBuilder::new(&ctx) + .activity(ActivityId::Open) + .action(ActionId::Allowed) + .disposition(DispositionId::Allowed) + .severity(SeverityId::Informational) + .src_endpoint_addr("10.42.0.1".parse().unwrap(), 48201) + .auth_type(AuthTypeId::Other, "NSSH1") + .protocol_ver("NSSH1") + .message("SSH handshake accepted via NSSH1") + .build(); + + let json = event.to_json(); + assert_eq!(json["class_uid"], 4007); + assert_eq!(json["auth_type"], "NSSH1"); + assert_eq!(json["auth_type_id"], 99); + } +} diff --git a/crates/openshell-ocsf/src/enums/action.rs b/crates/openshell-ocsf/src/enums/action.rs new file mode 100644 index 00000000..8a4638c1 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/action.rs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `action_id` enum. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Action ID (0-4, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ActionId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Allowed + Allowed = 1, + /// 2 — Denied + Denied = 2, + /// 3 — Alerted + Alerted = 3, + /// 4 — Dropped + Dropped = 4, + /// 99 — Other + Other = 99, +} + +impl ActionId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Allowed => "Allowed", + Self::Denied => "Denied", + Self::Alerted => "Alerted", + Self::Dropped => "Dropped", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_action_labels() { + assert_eq!(ActionId::Unknown.label(), "Unknown"); + assert_eq!(ActionId::Allowed.label(), "Allowed"); + assert_eq!(ActionId::Denied.label(), "Denied"); + assert_eq!(ActionId::Alerted.label(), "Alerted"); + assert_eq!(ActionId::Dropped.label(), "Dropped"); + assert_eq!(ActionId::Other.label(), "Other"); + } + + #[test] + fn test_action_json_roundtrip() { + let action = ActionId::Denied; + let json = serde_json::to_value(action).unwrap(); + assert_eq!(json, serde_json::json!(2)); + let deserialized: ActionId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, ActionId::Denied); + } +} diff --git a/crates/openshell-ocsf/src/enums/activity.rs b/crates/openshell-ocsf/src/enums/activity.rs new file mode 100644 index 00000000..e31fea85 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/activity.rs @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `activity_id` enum — unified across event classes. +//! +//! OCSF defines per-class activity IDs. We use a single enum with variants +//! covering all classes. The `class_uid` context determines which variants +//! are valid for a given event. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Activity ID — unified across event classes. +/// +/// Activity semantics vary by event class. The naming follows the most +/// common OCSF usage. See per-variant docs for which classes use each. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ActivityId { + /// 0 — Unknown (all classes) + Unknown = 0, + + // --- Network/SSH/HTTP Activity (4001, 4002, 4007) --- + // --- Also Detection Finding: Create (2004) --- + // --- Also Application Lifecycle: Install (6002) --- + // --- Also Config State Change: Log (5019) --- + /// 1 — Open (Network/SSH), Connect (HTTP), Create (Finding), Install (Lifecycle), Log (Config) + Open = 1, + /// 2 — Close (Network/SSH), Delete (HTTP), Update (Finding), Remove (Lifecycle), Collect (Config) + Close = 2, + /// 3 — Reset (Network), Get (HTTP), Close (Finding), Start (Lifecycle) + Reset = 3, + /// 4 — Fail (Network/SSH), Head (HTTP), Stop (Lifecycle) + Fail = 4, + /// 5 — Refuse (Network/SSH), Options (HTTP), Restart (Lifecycle) + Refuse = 5, + /// 6 — Traffic (Network), Post (HTTP), Enable (Lifecycle) + Traffic = 6, + /// 7 — Listen (Network/SSH), Put (HTTP), Disable (Lifecycle) + Listen = 7, + /// 8 — Trace (HTTP), Update (Lifecycle) + Trace = 8, + /// 9 — Patch (HTTP) + Patch = 9, + + /// 99 — Other (all classes) + Other = 99, +} + +impl ActivityId { + /// Returns a human-readable label for this activity in a network context. + #[must_use] + pub fn network_label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Open => "Open", + Self::Close => "Close", + Self::Reset => "Reset", + Self::Fail => "Fail", + Self::Refuse => "Refuse", + Self::Traffic => "Traffic", + Self::Listen => "Listen", + Self::Trace => "Trace", + Self::Patch => "Patch", + Self::Other => "Other", + } + } + + /// Returns a human-readable label for HTTP activity context. + #[must_use] + pub fn http_label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Open => "Connect", + Self::Close => "Delete", + Self::Reset => "Get", + Self::Fail => "Head", + Self::Refuse => "Options", + Self::Traffic => "Post", + Self::Listen => "Put", + Self::Trace => "Trace", + Self::Patch => "Patch", + Self::Other => "Other", + } + } + + /// Returns a human-readable label for Detection Finding activity context. + #[must_use] + pub fn finding_label(self) -> &'static str { + match self { + Self::Open => "Create", + Self::Close => "Update", + Self::Reset => "Close", + _ => self.network_label(), + } + } + + /// Returns a human-readable label for Application Lifecycle activity context. + #[must_use] + pub fn lifecycle_label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Open => "Install", + Self::Close => "Remove", + Self::Reset => "Start", + Self::Fail => "Stop", + Self::Refuse => "Restart", + Self::Traffic => "Enable", + Self::Listen => "Disable", + Self::Trace => "Update", + Self::Patch | Self::Other => "Other", + } + } + + /// Returns a human-readable label for Config State Change activity context. + #[must_use] + pub fn config_label(self) -> &'static str { + match self { + Self::Open => "Log", + Self::Close => "Collect", + _ => self.network_label(), + } + } + + /// Returns a human-readable label for Process Activity context. + #[must_use] + pub fn process_label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Open => "Launch", + Self::Close => "Terminate", + Self::Reset => "Open", + Self::Fail => "Inject", + Self::Refuse => "Set User ID", + _ => self.network_label(), + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_activity_network_labels() { + assert_eq!(ActivityId::Open.network_label(), "Open"); + assert_eq!(ActivityId::Close.network_label(), "Close"); + assert_eq!(ActivityId::Refuse.network_label(), "Refuse"); + assert_eq!(ActivityId::Listen.network_label(), "Listen"); + } + + #[test] + fn test_activity_http_labels() { + assert_eq!(ActivityId::Open.http_label(), "Connect"); + assert_eq!(ActivityId::Close.http_label(), "Delete"); + assert_eq!(ActivityId::Reset.http_label(), "Get"); + assert_eq!(ActivityId::Traffic.http_label(), "Post"); + assert_eq!(ActivityId::Listen.http_label(), "Put"); + assert_eq!(ActivityId::Patch.http_label(), "Patch"); + } + + #[test] + fn test_activity_process_labels() { + assert_eq!(ActivityId::Open.process_label(), "Launch"); + assert_eq!(ActivityId::Close.process_label(), "Terminate"); + } + + #[test] + fn test_activity_json_roundtrip() { + let activity = ActivityId::Open; + let json = serde_json::to_value(activity).unwrap(); + assert_eq!(json, serde_json::json!(1)); + let deserialized: ActivityId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, ActivityId::Open); + } +} diff --git a/crates/openshell-ocsf/src/enums/auth.rs b/crates/openshell-ocsf/src/enums/auth.rs new file mode 100644 index 00000000..5b72fe3f --- /dev/null +++ b/crates/openshell-ocsf/src/enums/auth.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `auth_type_id` enum for SSH Activity. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Auth Type ID (0-6, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum AuthTypeId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Certificate Based + CertificateBased = 1, + /// 2 — GSSAPI + Gssapi = 2, + /// 3 — Host Based + HostBased = 3, + /// 4 — Keyboard Interactive + KeyboardInteractive = 4, + /// 5 — Password + Password = 5, + /// 6 — Public Key + PublicKey = 6, + /// 99 — Other (used for NSSH1) + Other = 99, +} + +impl AuthTypeId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::CertificateBased => "Certificate Based", + Self::Gssapi => "GSSAPI", + Self::HostBased => "Host Based", + Self::KeyboardInteractive => "Keyboard Interactive", + Self::Password => "Password", + Self::PublicKey => "Public Key", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_auth_type_labels() { + assert_eq!(AuthTypeId::Unknown.label(), "Unknown"); + assert_eq!(AuthTypeId::CertificateBased.label(), "Certificate Based"); + assert_eq!(AuthTypeId::PublicKey.label(), "Public Key"); + assert_eq!(AuthTypeId::Other.label(), "Other"); + } + + #[test] + fn test_auth_type_integer_values() { + assert_eq!(AuthTypeId::Unknown.as_u8(), 0); + assert_eq!(AuthTypeId::CertificateBased.as_u8(), 1); + assert_eq!(AuthTypeId::PublicKey.as_u8(), 6); + assert_eq!(AuthTypeId::Other.as_u8(), 99); + } + + #[test] + fn test_auth_type_json_roundtrip() { + let auth = AuthTypeId::Other; + let json = serde_json::to_value(auth).unwrap(); + assert_eq!(json, serde_json::json!(99)); + let deserialized: AuthTypeId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, AuthTypeId::Other); + } +} diff --git a/crates/openshell-ocsf/src/enums/disposition.rs b/crates/openshell-ocsf/src/enums/disposition.rs new file mode 100644 index 00000000..8e4078a1 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/disposition.rs @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `disposition_id` enum. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Disposition ID (0-27, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum DispositionId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Allowed + Allowed = 1, + /// 2 — Blocked + Blocked = 2, + /// 3 — Quarantined + Quarantined = 3, + /// 4 — Isolated + Isolated = 4, + /// 5 — Deleted + Deleted = 5, + /// 6 — Dropped + Dropped = 6, + /// 7 — Custom Action + CustomAction = 7, + /// 8 — Approved + Approved = 8, + /// 9 — Restored + Restored = 9, + /// 10 — Exonerated + Exonerated = 10, + /// 11 — Corrected + Corrected = 11, + /// 12 — Partially Corrected + PartiallyCorrected = 12, + /// 13 — Uncorrected + Uncorrected = 13, + /// 14 — Delayed + Delayed = 14, + /// 15 — Detected + Detected = 15, + /// 16 — No Action + NoAction = 16, + /// 17 — Logged + Logged = 17, + /// 18 — Tagged + Tagged = 18, + /// 19 — Alert + Alert = 19, + /// 20 — Count + Count = 20, + /// 21 — Reset + Reset = 21, + /// 22 — Captcha + Captcha = 22, + /// 23 — Challenge + Challenge = 23, + /// 24 — Access Revoked + AccessRevoked = 24, + /// 25 — Rejected + Rejected = 25, + /// 26 — Unauthorized + Unauthorized = 26, + /// 27 — Error + Error = 27, + /// 99 — Other + Other = 99, +} + +impl DispositionId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Allowed => "Allowed", + Self::Blocked => "Blocked", + Self::Quarantined => "Quarantined", + Self::Isolated => "Isolated", + Self::Deleted => "Deleted", + Self::Dropped => "Dropped", + Self::CustomAction => "Custom Action", + Self::Approved => "Approved", + Self::Restored => "Restored", + Self::Exonerated => "Exonerated", + Self::Corrected => "Corrected", + Self::PartiallyCorrected => "Partially Corrected", + Self::Uncorrected => "Uncorrected", + Self::Delayed => "Delayed", + Self::Detected => "Detected", + Self::NoAction => "No Action", + Self::Logged => "Logged", + Self::Tagged => "Tagged", + Self::Alert => "Alert", + Self::Count => "Count", + Self::Reset => "Reset", + Self::Captcha => "Captcha", + Self::Challenge => "Challenge", + Self::AccessRevoked => "Access Revoked", + Self::Rejected => "Rejected", + Self::Unauthorized => "Unauthorized", + Self::Error => "Error", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_disposition_labels() { + assert_eq!(DispositionId::Unknown.label(), "Unknown"); + assert_eq!(DispositionId::Allowed.label(), "Allowed"); + assert_eq!(DispositionId::Blocked.label(), "Blocked"); + assert_eq!(DispositionId::Detected.label(), "Detected"); + assert_eq!(DispositionId::Logged.label(), "Logged"); + assert_eq!(DispositionId::Rejected.label(), "Rejected"); + assert_eq!(DispositionId::Error.label(), "Error"); + assert_eq!(DispositionId::Other.label(), "Other"); + } + + #[test] + fn test_disposition_integer_values() { + assert_eq!(DispositionId::Unknown.as_u8(), 0); + assert_eq!(DispositionId::Allowed.as_u8(), 1); + assert_eq!(DispositionId::Blocked.as_u8(), 2); + assert_eq!(DispositionId::Rejected.as_u8(), 25); + assert_eq!(DispositionId::Error.as_u8(), 27); + assert_eq!(DispositionId::Other.as_u8(), 99); + } + + #[test] + fn test_disposition_json_roundtrip() { + let disp = DispositionId::Blocked; + let json = serde_json::to_value(disp).unwrap(); + assert_eq!(json, serde_json::json!(2)); + let deserialized: DispositionId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, DispositionId::Blocked); + } +} diff --git a/crates/openshell-ocsf/src/enums/launch.rs b/crates/openshell-ocsf/src/enums/launch.rs new file mode 100644 index 00000000..71e6823a --- /dev/null +++ b/crates/openshell-ocsf/src/enums/launch.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `launch_type_id` enum (new in v1.7.0). + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Launch Type ID (0-3, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum LaunchTypeId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Spawn + Spawn = 1, + /// 2 — Fork + Fork = 2, + /// 3 — Exec + Exec = 3, + /// 99 — Other + Other = 99, +} + +impl LaunchTypeId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Spawn => "Spawn", + Self::Fork => "Fork", + Self::Exec => "Exec", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_launch_type_labels() { + assert_eq!(LaunchTypeId::Unknown.label(), "Unknown"); + assert_eq!(LaunchTypeId::Spawn.label(), "Spawn"); + assert_eq!(LaunchTypeId::Fork.label(), "Fork"); + assert_eq!(LaunchTypeId::Exec.label(), "Exec"); + assert_eq!(LaunchTypeId::Other.label(), "Other"); + } + + #[test] + fn test_launch_type_json_roundtrip() { + let launch = LaunchTypeId::Spawn; + let json = serde_json::to_value(launch).unwrap(); + assert_eq!(json, serde_json::json!(1)); + let deserialized: LaunchTypeId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, LaunchTypeId::Spawn); + } +} diff --git a/crates/openshell-ocsf/src/enums/mod.rs b/crates/openshell-ocsf/src/enums/mod.rs new file mode 100644 index 00000000..2bddd77e --- /dev/null +++ b/crates/openshell-ocsf/src/enums/mod.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF v1.7.0 enum types. + +mod action; +mod activity; +mod auth; +mod disposition; +mod launch; +mod security; +mod severity; +mod status; + +pub use action::ActionId; +pub use activity::ActivityId; +pub use auth::AuthTypeId; +pub use disposition::DispositionId; +pub use launch::LaunchTypeId; +pub use security::{ConfidenceId, RiskLevelId, SecurityLevelId}; +pub use severity::SeverityId; +pub use status::{StateId, StatusId}; diff --git a/crates/openshell-ocsf/src/enums/security.rs b/crates/openshell-ocsf/src/enums/security.rs new file mode 100644 index 00000000..b0fcffa3 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/security.rs @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF security-related enums: `security_level_id`, `confidence_id`, `risk_level_id`. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Security Level ID (0-3, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum SecurityLevelId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Secure + Secure = 1, + /// 2 — At Risk + AtRisk = 2, + /// 3 — Compromised + Compromised = 3, + /// 99 — Other + Other = 99, +} + +impl SecurityLevelId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Secure => "Secure", + Self::AtRisk => "At Risk", + Self::Compromised => "Compromised", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +/// OCSF Confidence ID (0-3, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ConfidenceId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Low + Low = 1, + /// 2 — Medium + Medium = 2, + /// 3 — High + High = 3, + /// 99 — Other + Other = 99, +} + +impl ConfidenceId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Low => "Low", + Self::Medium => "Medium", + Self::High => "High", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +/// OCSF Risk Level ID (0-4, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum RiskLevelId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Info + Info = 1, + /// 2 — Low + Low = 2, + /// 3 — Medium + Medium = 3, + /// 4 — High + High = 4, + /// 5 — Critical + Critical = 5, + /// 99 — Other + Other = 99, +} + +impl RiskLevelId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Info => "Info", + Self::Low => "Low", + Self::Medium => "Medium", + Self::High => "High", + Self::Critical => "Critical", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_security_level_labels() { + assert_eq!(SecurityLevelId::Unknown.label(), "Unknown"); + assert_eq!(SecurityLevelId::Secure.label(), "Secure"); + assert_eq!(SecurityLevelId::AtRisk.label(), "At Risk"); + assert_eq!(SecurityLevelId::Compromised.label(), "Compromised"); + } + + #[test] + fn test_confidence_labels() { + assert_eq!(ConfidenceId::Unknown.label(), "Unknown"); + assert_eq!(ConfidenceId::Low.label(), "Low"); + assert_eq!(ConfidenceId::Medium.label(), "Medium"); + assert_eq!(ConfidenceId::High.label(), "High"); + } + + #[test] + fn test_risk_level_labels() { + assert_eq!(RiskLevelId::Unknown.label(), "Unknown"); + assert_eq!(RiskLevelId::Info.label(), "Info"); + assert_eq!(RiskLevelId::Low.label(), "Low"); + assert_eq!(RiskLevelId::Medium.label(), "Medium"); + assert_eq!(RiskLevelId::High.label(), "High"); + assert_eq!(RiskLevelId::Critical.label(), "Critical"); + } + + #[test] + fn test_security_json_roundtrips() { + let sl = SecurityLevelId::Secure; + let json = serde_json::to_value(sl).unwrap(); + assert_eq!(json, serde_json::json!(1)); + + let conf = ConfidenceId::High; + let json = serde_json::to_value(conf).unwrap(); + assert_eq!(json, serde_json::json!(3)); + + let risk = RiskLevelId::High; + let json = serde_json::to_value(risk).unwrap(); + assert_eq!(json, serde_json::json!(4)); + } +} diff --git a/crates/openshell-ocsf/src/enums/severity.rs b/crates/openshell-ocsf/src/enums/severity.rs new file mode 100644 index 00000000..4609e0cc --- /dev/null +++ b/crates/openshell-ocsf/src/enums/severity.rs @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `severity_id` enum. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Severity ID (0-6, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum SeverityId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Informational + Informational = 1, + /// 2 — Low + Low = 2, + /// 3 — Medium + Medium = 3, + /// 4 — High + High = 4, + /// 5 — Critical + Critical = 5, + /// 6 — Fatal + Fatal = 6, + /// 99 — Other + Other = 99, +} + +impl SeverityId { + /// Returns the OCSF string label for this severity. + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Informational => "Informational", + Self::Low => "Low", + Self::Medium => "Medium", + Self::High => "High", + Self::Critical => "Critical", + Self::Fatal => "Fatal", + Self::Other => "Other", + } + } + + /// Returns the single-character shorthand for log display. + #[must_use] + pub fn shorthand_char(self) -> char { + match self { + Self::Informational => 'I', + Self::Low => 'L', + Self::Medium => 'M', + Self::High => 'H', + Self::Critical => 'C', + Self::Fatal => 'F', + Self::Unknown | Self::Other => ' ', + } + } + + /// Returns the integer value for JSON serialization. + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_severity_labels() { + assert_eq!(SeverityId::Unknown.label(), "Unknown"); + assert_eq!(SeverityId::Informational.label(), "Informational"); + assert_eq!(SeverityId::Low.label(), "Low"); + assert_eq!(SeverityId::Medium.label(), "Medium"); + assert_eq!(SeverityId::High.label(), "High"); + assert_eq!(SeverityId::Critical.label(), "Critical"); + assert_eq!(SeverityId::Fatal.label(), "Fatal"); + assert_eq!(SeverityId::Other.label(), "Other"); + } + + #[test] + fn test_severity_shorthand_chars() { + assert_eq!(SeverityId::Unknown.shorthand_char(), ' '); + assert_eq!(SeverityId::Informational.shorthand_char(), 'I'); + assert_eq!(SeverityId::Low.shorthand_char(), 'L'); + assert_eq!(SeverityId::Medium.shorthand_char(), 'M'); + assert_eq!(SeverityId::High.shorthand_char(), 'H'); + assert_eq!(SeverityId::Critical.shorthand_char(), 'C'); + assert_eq!(SeverityId::Fatal.shorthand_char(), 'F'); + assert_eq!(SeverityId::Other.shorthand_char(), ' '); + } + + #[test] + fn test_severity_integer_values() { + assert_eq!(SeverityId::Unknown.as_u8(), 0); + assert_eq!(SeverityId::Informational.as_u8(), 1); + assert_eq!(SeverityId::Low.as_u8(), 2); + assert_eq!(SeverityId::Medium.as_u8(), 3); + assert_eq!(SeverityId::High.as_u8(), 4); + assert_eq!(SeverityId::Critical.as_u8(), 5); + assert_eq!(SeverityId::Fatal.as_u8(), 6); + assert_eq!(SeverityId::Other.as_u8(), 99); + } + + #[test] + fn test_severity_json_roundtrip() { + let severity = SeverityId::High; + let json = serde_json::to_value(severity).unwrap(); + assert_eq!(json, serde_json::json!(4)); + let deserialized: SeverityId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, SeverityId::High); + } +} diff --git a/crates/openshell-ocsf/src/enums/status.rs b/crates/openshell-ocsf/src/enums/status.rs new file mode 100644 index 00000000..90ac4c27 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/status.rs @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `status_id` and `state_id` enums. + +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// OCSF Status ID (0-2, 99). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum StatusId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Success + Success = 1, + /// 2 — Failure + Failure = 2, + /// 99 — Other + Other = 99, +} + +impl StatusId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Success => "Success", + Self::Failure => "Failure", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +/// OCSF State ID (0-2, 99) — used by Device Config State Change. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum StateId { + /// 0 — Unknown + Unknown = 0, + /// 1 — Disabled + Disabled = 1, + /// 2 — Enabled + Enabled = 2, + /// 99 — Other + Other = 99, +} + +impl StateId { + #[must_use] + pub fn label(self) -> &'static str { + match self { + Self::Unknown => "Unknown", + Self::Disabled => "Disabled", + Self::Enabled => "Enabled", + Self::Other => "Other", + } + } + + #[must_use] + pub fn as_u8(self) -> u8 { + self as u8 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_status_labels() { + assert_eq!(StatusId::Unknown.label(), "Unknown"); + assert_eq!(StatusId::Success.label(), "Success"); + assert_eq!(StatusId::Failure.label(), "Failure"); + assert_eq!(StatusId::Other.label(), "Other"); + } + + #[test] + fn test_status_json_roundtrip() { + let status = StatusId::Success; + let json = serde_json::to_value(status).unwrap(); + assert_eq!(json, serde_json::json!(1)); + let deserialized: StatusId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, StatusId::Success); + } + + #[test] + fn test_state_labels() { + assert_eq!(StateId::Unknown.label(), "Unknown"); + assert_eq!(StateId::Disabled.label(), "Disabled"); + assert_eq!(StateId::Enabled.label(), "Enabled"); + assert_eq!(StateId::Other.label(), "Other"); + } + + #[test] + fn test_state_json_roundtrip() { + let state = StateId::Enabled; + let json = serde_json::to_value(state).unwrap(); + assert_eq!(json, serde_json::json!(2)); + let deserialized: StateId = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, StateId::Enabled); + } +} diff --git a/crates/openshell-ocsf/src/events/app_lifecycle.rs b/crates/openshell-ocsf/src/events/app_lifecycle.rs new file mode 100644 index 00000000..4dc074eb --- /dev/null +++ b/crates/openshell-ocsf/src/events/app_lifecycle.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF Application Lifecycle [6002] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; +use crate::objects::Product; + +/// OCSF Application Lifecycle Event [6002]. +/// +/// Sandbox supervisor lifecycle events. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ApplicationLifecycleEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// Application / product info (required). + pub app: Product, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::SeverityId; + use crate::objects::Metadata; + + #[test] + fn test_app_lifecycle_serialization() { + let mut base = BaseEventData::new( + 6002, + "Application Lifecycle", + 6, + "Application Activity", + 3, + "Start", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["container".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ); + base.set_message("Starting sandbox"); + + let event = ApplicationLifecycleEvent { + base, + app: Product::openshell_sandbox("0.1.0"), + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 6002); + assert_eq!(json["type_uid"], 600_203); + assert_eq!(json["app"]["name"], "OpenShell Sandbox Supervisor"); + assert_eq!(json["message"], "Starting sandbox"); + } +} diff --git a/crates/openshell-ocsf/src/events/base_event.rs b/crates/openshell-ocsf/src/events/base_event.rs new file mode 100644 index 00000000..f8568fe6 --- /dev/null +++ b/crates/openshell-ocsf/src/events/base_event.rs @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF Base Event [0] and shared `BaseEventData`. + +use serde::{Deserialize, Serialize}; + +use crate::enums::{SeverityId, StatusId}; +use crate::objects::{Container, Device, Metadata}; + +/// Common fields shared by all OCSF event classes. +/// +/// Every event class embeds this struct via `#[serde(flatten)]`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BaseEventData { + /// OCSF class UID (e.g., 4001 for Network Activity). + pub class_uid: u32, + + /// Human-readable class name. + pub class_name: String, + + /// OCSF category UID. + pub category_uid: u8, + + /// Human-readable category name. + pub category_name: String, + + /// Activity ID within the class. + pub activity_id: u8, + + /// Human-readable activity name. + pub activity_name: String, + + /// Computed type UID: `class_uid * 100 + activity_id`. + pub type_uid: u32, + + /// Human-readable type name: "`class_name`: `activity_name`". + pub type_name: String, + + /// Event timestamp in milliseconds since epoch. + pub time: i64, + + /// Severity ID. + pub severity_id: u8, + + /// Severity label. + pub severity: String, + + /// Status ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub status_id: Option, + + /// Status label. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + + /// Human-readable event message. + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Status detail / reason. + #[serde(skip_serializing_if = "Option::is_none")] + pub status_detail: Option, + + /// Event metadata (schema version, product, profiles). + pub metadata: Metadata, + + /// Device info. + #[serde(skip_serializing_if = "Option::is_none")] + pub device: Option, + + /// Container info (Container profile). + #[serde(skip_serializing_if = "Option::is_none")] + pub container: Option, + + /// Unmapped fields that don't fit the OCSF schema. + #[serde(skip_serializing_if = "Option::is_none")] + pub unmapped: Option, +} + +impl BaseEventData { + /// Create base event data with required fields. + #[allow(clippy::too_many_arguments)] + #[must_use] + pub fn new( + class_uid: u32, + class_name: &str, + category_uid: u8, + category_name: &str, + activity_id: u8, + activity_name: &str, + severity_id: SeverityId, + metadata: Metadata, + ) -> Self { + let type_uid = class_uid * 100 + u32::from(activity_id); + let type_name = format!("{class_name}: {activity_name}"); + + Self { + class_uid, + class_name: class_name.to_string(), + category_uid, + category_name: category_name.to_string(), + activity_id, + activity_name: activity_name.to_string(), + type_uid, + type_name, + time: chrono::Utc::now().timestamp_millis(), + severity_id: severity_id.as_u8(), + severity: severity_id.label().to_string(), + status_id: None, + status: None, + message: None, + status_detail: None, + metadata, + device: None, + container: None, + unmapped: None, + } + } + + /// Set the timestamp (milliseconds since epoch). + pub fn set_time(&mut self, time_ms: i64) { + self.time = time_ms; + } + + /// Set status. + pub fn set_status(&mut self, status_id: StatusId) { + self.status_id = Some(status_id.as_u8()); + self.status = Some(status_id.label().to_string()); + } + + /// Set message. + pub fn set_message(&mut self, message: impl Into) { + self.message = Some(message.into()); + } + + /// Set status detail. + pub fn set_status_detail(&mut self, detail: impl Into) { + self.status_detail = Some(detail.into()); + } + + /// Set device info. + pub fn set_device(&mut self, device: Device) { + self.device = Some(device); + } + + /// Set container info. + pub fn set_container(&mut self, container: Container) { + self.container = Some(container); + } + + /// Add an unmapped field. + pub fn add_unmapped(&mut self, key: &str, value: impl Into) { + let map = self + .unmapped + .get_or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); + if let serde_json::Value::Object(m) = map { + m.insert(key.to_string(), value.into()); + } + } +} + +/// OCSF Base Event [0] — for events that don't fit a specific class. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BaseEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::objects::Product; + + fn test_metadata() -> Metadata { + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["container".to_string(), "host".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + } + } + + #[test] + fn test_base_event_data_creation() { + let base = BaseEventData::new( + 0, + "Base Event", + 0, + "Uncategorized", + 99, + "Other", + SeverityId::Informational, + test_metadata(), + ); + + assert_eq!(base.class_uid, 0); + assert_eq!(base.type_uid, 99); // 0 * 100 + 99 + assert_eq!(base.type_name, "Base Event: Other"); + assert_eq!(base.severity_id, 1); + assert_eq!(base.severity, "Informational"); + } + + #[test] + fn test_type_uid_computation() { + let base = BaseEventData::new( + 4001, + "Network Activity", + 4, + "Network Activity", + 1, + "Open", + SeverityId::Informational, + test_metadata(), + ); + + assert_eq!(base.type_uid, 400_101); // 4001 * 100 + 1 + } + + #[test] + fn test_base_event_serialization() { + let mut base = BaseEventData::new( + 0, + "Base Event", + 0, + "Uncategorized", + 99, + "Network Namespace Created", + SeverityId::Informational, + test_metadata(), + ); + base.set_status(StatusId::Success); + base.set_message("Network namespace created"); + base.add_unmapped("namespace", serde_json::json!("openshell-sandbox-abc123")); + + let event = BaseEvent { base }; + let json = serde_json::to_value(&event).unwrap(); + + assert_eq!(json["class_uid"], 0); + assert_eq!(json["class_name"], "Base Event"); + assert_eq!(json["activity_name"], "Network Namespace Created"); + assert_eq!(json["status"], "Success"); + assert_eq!(json["message"], "Network namespace created"); + assert_eq!(json["unmapped"]["namespace"], "openshell-sandbox-abc123"); + } +} diff --git a/crates/openshell-ocsf/src/events/config_state_change.rs b/crates/openshell-ocsf/src/events/config_state_change.rs new file mode 100644 index 00000000..8425be72 --- /dev/null +++ b/crates/openshell-ocsf/src/events/config_state_change.rs @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF Device Config State Change [5019] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; + +/// OCSF Device Config State Change Event [5019]. +/// +/// Policy engine and inference routing configuration changes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeviceConfigStateChangeEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// State ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_id: Option, + + /// State label. + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + + /// Security level ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub security_level_id: Option, + + /// Security level label. + #[serde(skip_serializing_if = "Option::is_none")] + pub security_level: Option, + + /// Previous security level ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_security_level_id: Option, + + /// Previous security level label. + #[serde(skip_serializing_if = "Option::is_none")] + pub prev_security_level: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::{SecurityLevelId, SeverityId, StateId}; + use crate::objects::{Metadata, Product}; + + #[test] + fn test_config_state_change_serialization() { + let mut base = BaseEventData::new( + 5019, + "Device Config State Change", + 5, + "Discovery", + 1, + "Log", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ); + base.set_message("Policy reloaded successfully"); + base.add_unmapped("policy_version", serde_json::json!("v3")); + base.add_unmapped("policy_hash", serde_json::json!("sha256:abc123def456")); + + let event = DeviceConfigStateChangeEvent { + base, + state_id: Some(StateId::Enabled.as_u8()), + state: Some("Enabled".to_string()), + security_level_id: Some(SecurityLevelId::Secure.as_u8()), + security_level: Some("Secure".to_string()), + prev_security_level_id: Some(SecurityLevelId::Unknown.as_u8()), + prev_security_level: Some("Unknown".to_string()), + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 5019); + assert_eq!(json["state_id"], 2); + assert_eq!(json["state"], "Enabled"); + assert_eq!(json["security_level"], "Secure"); + assert_eq!(json["unmapped"]["policy_version"], "v3"); + } +} diff --git a/crates/openshell-ocsf/src/events/detection_finding.rs b/crates/openshell-ocsf/src/events/detection_finding.rs new file mode 100644 index 00000000..b2a0e585 --- /dev/null +++ b/crates/openshell-ocsf/src/events/detection_finding.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF Detection Finding [2004] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; +use crate::objects::{Attack, Evidence, FindingInfo, Remediation}; + +/// OCSF Detection Finding Event [2004]. +/// +/// Security-relevant findings from policy enforcement. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DetectionFindingEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// Finding details (required). + pub finding_info: FindingInfo, + + /// Evidence artifacts. + #[serde(skip_serializing_if = "Option::is_none")] + pub evidences: Option>, + + /// MITRE ATT&CK mappings. + #[serde(skip_serializing_if = "Option::is_none")] + pub attacks: Option>, + + /// Remediation guidance. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, + + /// Whether this finding is an alert. + #[serde(skip_serializing_if = "Option::is_none")] + pub is_alert: Option, + + /// Confidence ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub confidence_id: Option, + + /// Confidence label. + #[serde(skip_serializing_if = "Option::is_none")] + pub confidence: Option, + + /// Risk level ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub risk_level_id: Option, + + /// Risk level label. + #[serde(skip_serializing_if = "Option::is_none")] + pub risk_level: Option, + + /// Action ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub action_id: Option, + + /// Action label. + #[serde(skip_serializing_if = "Option::is_none")] + pub action: Option, + + /// Disposition ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition_id: Option, + + /// Disposition label. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::SeverityId; + use crate::objects::{Metadata, Product}; + + #[test] + fn test_detection_finding_serialization() { + let event = DetectionFindingEvent { + base: BaseEventData::new( + 2004, + "Detection Finding", + 2, + "Findings", + 1, + "Create", + SeverityId::High, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ), + finding_info: FindingInfo::new("nssh1-replay-abc", "NSSH1 Nonce Replay Attack") + .with_desc("A nonce was replayed."), + evidences: Some(vec![Evidence::from_pairs(&[ + ("nonce", "0xdeadbeef"), + ("peer_ip", "10.42.0.1"), + ])]), + attacks: Some(vec![Attack::mitre( + "T1550", + "Use Alternate Authentication Material", + "TA0008", + "Lateral Movement", + )]), + remediation: None, + is_alert: Some(true), + confidence_id: Some(3), + confidence: Some("High".to_string()), + risk_level_id: Some(4), + risk_level: Some("High".to_string()), + action_id: Some(2), + action: Some("Denied".to_string()), + disposition_id: Some(2), + disposition: Some("Blocked".to_string()), + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 2004); + assert_eq!(json["finding_info"]["title"], "NSSH1 Nonce Replay Attack"); + assert_eq!(json["is_alert"], true); + assert_eq!(json["confidence"], "High"); + assert_eq!(json["attacks"][0]["technique"]["uid"], "T1550"); + } +} diff --git a/crates/openshell-ocsf/src/events/http_activity.rs b/crates/openshell-ocsf/src/events/http_activity.rs new file mode 100644 index 00000000..060f5f66 --- /dev/null +++ b/crates/openshell-ocsf/src/events/http_activity.rs @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF HTTP Activity [4002] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; +use crate::objects::{Actor, Endpoint, FirewallRule, HttpRequest, HttpResponse}; + +/// OCSF HTTP Activity Event [4002]. +/// +/// HTTP-level events through the forward proxy and L7 relay. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HttpActivityEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// HTTP request details. + #[serde(skip_serializing_if = "Option::is_none")] + pub http_request: Option, + + /// HTTP response details. + #[serde(skip_serializing_if = "Option::is_none")] + pub http_response: Option, + + /// Source endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub src_endpoint: Option, + + /// Destination endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_endpoint: Option, + + /// Proxy endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub proxy_endpoint: Option, + + /// Actor (process that made the request). + #[serde(skip_serializing_if = "Option::is_none")] + pub actor: Option, + + /// Firewall / policy rule. + #[serde(skip_serializing_if = "Option::is_none")] + pub firewall_rule: Option, + + /// Action ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub action_id: Option, + + /// Action label. + #[serde(skip_serializing_if = "Option::is_none")] + pub action: Option, + + /// Disposition ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition_id: Option, + + /// Disposition label. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition: Option, + + /// Observation point ID (v1.6.0+). + #[serde(skip_serializing_if = "Option::is_none")] + pub observation_point_id: Option, + + /// Whether src/dst assignment is known (v1.6.0+). + #[serde(skip_serializing_if = "Option::is_none")] + pub is_src_dst_assignment_known: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::SeverityId; + use crate::objects::{Metadata, Product, Url}; + + #[test] + fn test_http_activity_serialization() { + let event = HttpActivityEvent { + base: BaseEventData::new( + 4002, + "HTTP Activity", + 4, + "Network Activity", + 3, + "Get", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ), + http_request: Some(HttpRequest::new( + "GET", + Url::new("https", "api.example.com", "/v1/data", 443), + )), + http_response: None, + src_endpoint: None, + dst_endpoint: Some(Endpoint::from_domain("api.example.com", 443)), + proxy_endpoint: None, + actor: None, + firewall_rule: None, + action_id: Some(1), + action: Some("Allowed".to_string()), + disposition_id: Some(1), + disposition: Some("Allowed".to_string()), + observation_point_id: None, + is_src_dst_assignment_known: None, + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 4002); + assert_eq!(json["type_uid"], 400_203); + assert_eq!(json["http_request"]["http_method"], "GET"); + } +} diff --git a/crates/openshell-ocsf/src/events/mod.rs b/crates/openshell-ocsf/src/events/mod.rs new file mode 100644 index 00000000..6af68c18 --- /dev/null +++ b/crates/openshell-ocsf/src/events/mod.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF v1.7.0 event class definitions. + +mod app_lifecycle; +pub(crate) mod base_event; +mod config_state_change; +mod detection_finding; +mod http_activity; +mod network_activity; +mod process_activity; +mod ssh_activity; + +pub use app_lifecycle::ApplicationLifecycleEvent; +pub use base_event::{BaseEvent, BaseEventData}; +pub use config_state_change::DeviceConfigStateChangeEvent; +pub use detection_finding::DetectionFindingEvent; +pub use http_activity::HttpActivityEvent; +pub use network_activity::NetworkActivityEvent; +pub use process_activity::ProcessActivityEvent; +pub use ssh_activity::SshActivityEvent; + +use serde::{Deserialize, Serialize}; + +/// Top-level OCSF event enum encompassing all supported event classes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OcsfEvent { + /// Network Activity [4001] + NetworkActivity(NetworkActivityEvent), + /// HTTP Activity [4002] + HttpActivity(HttpActivityEvent), + /// SSH Activity [4007] + SshActivity(SshActivityEvent), + /// Process Activity [1007] + ProcessActivity(ProcessActivityEvent), + /// Detection Finding [2004] + DetectionFinding(DetectionFindingEvent), + /// Application Lifecycle [6002] + ApplicationLifecycle(ApplicationLifecycleEvent), + /// Device Config State Change [5019] + DeviceConfigStateChange(DeviceConfigStateChangeEvent), + /// Base Event [0] + Base(BaseEvent), +} + +impl OcsfEvent { + /// Returns the OCSF `class_uid` for this event. + #[must_use] + pub fn class_uid(&self) -> u32 { + match self { + Self::NetworkActivity(_) => 4001, + Self::HttpActivity(_) => 4002, + Self::SshActivity(_) => 4007, + Self::ProcessActivity(_) => 1007, + Self::DetectionFinding(_) => 2004, + Self::ApplicationLifecycle(_) => 6002, + Self::DeviceConfigStateChange(_) => 5019, + Self::Base(_) => 0, + } + } + + /// Returns the base event data common to all event classes. + #[must_use] + pub fn base(&self) -> &BaseEventData { + match self { + Self::NetworkActivity(e) => &e.base, + Self::HttpActivity(e) => &e.base, + Self::SshActivity(e) => &e.base, + Self::ProcessActivity(e) => &e.base, + Self::DetectionFinding(e) => &e.base, + Self::ApplicationLifecycle(e) => &e.base, + Self::DeviceConfigStateChange(e) => &e.base, + Self::Base(e) => &e.base, + } + } +} diff --git a/crates/openshell-ocsf/src/events/network_activity.rs b/crates/openshell-ocsf/src/events/network_activity.rs new file mode 100644 index 00000000..0d611eb2 --- /dev/null +++ b/crates/openshell-ocsf/src/events/network_activity.rs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF Network Activity [4001] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; +use crate::objects::{Actor, ConnectionInfo, Endpoint, FirewallRule}; + +/// OCSF Network Activity Event [4001]. +/// +/// Proxy CONNECT tunnel events and iptables-level bypass detection. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NetworkActivityEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// Source endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub src_endpoint: Option, + + /// Destination endpoint. + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_endpoint: Option, + + /// Proxy endpoint (Network Proxy profile). + #[serde(skip_serializing_if = "Option::is_none")] + pub proxy_endpoint: Option, + + /// Actor (process that initiated the connection). + #[serde(skip_serializing_if = "Option::is_none")] + pub actor: Option, + + /// Firewall / policy rule that applied. + #[serde(skip_serializing_if = "Option::is_none")] + pub firewall_rule: Option, + + /// Connection info (protocol name). + #[serde(skip_serializing_if = "Option::is_none")] + pub connection_info: Option, + + /// Action ID (Security Control profile). + #[serde(skip_serializing_if = "Option::is_none")] + pub action_id: Option, + + /// Action label. + #[serde(skip_serializing_if = "Option::is_none")] + pub action: Option, + + /// Disposition ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition_id: Option, + + /// Disposition label. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition: Option, + + /// Observation point ID (v1.6.0+). + #[serde(skip_serializing_if = "Option::is_none")] + pub observation_point_id: Option, + + /// Whether src/dst assignment is known (v1.6.0+). + #[serde(skip_serializing_if = "Option::is_none")] + pub is_src_dst_assignment_known: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::{ActionId, DispositionId, SeverityId}; + use crate::objects::{Metadata, Product}; + + #[test] + fn test_network_activity_serialization() { + let event = NetworkActivityEvent { + base: BaseEventData::new( + 4001, + "Network Activity", + 4, + "Network Activity", + 1, + "Open", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string(), "network_proxy".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ), + src_endpoint: Some(Endpoint::from_ip_str("10.42.0.2", 54321)), + dst_endpoint: Some(Endpoint::from_domain("api.example.com", 443)), + proxy_endpoint: Some(Endpoint::from_ip_str("10.42.0.1", 3128)), + actor: None, + firewall_rule: Some(FirewallRule::new("default-egress", "mechanistic")), + connection_info: None, + action_id: Some(ActionId::Allowed.as_u8()), + action: Some(ActionId::Allowed.label().to_string()), + disposition_id: Some(DispositionId::Allowed.as_u8()), + disposition: Some(DispositionId::Allowed.label().to_string()), + observation_point_id: Some(2), + is_src_dst_assignment_known: Some(true), + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 4001); + assert_eq!(json["class_name"], "Network Activity"); + assert_eq!(json["type_uid"], 400_101); + assert_eq!(json["action"], "Allowed"); + assert_eq!(json["disposition"], "Allowed"); + assert_eq!(json["dst_endpoint"]["domain"], "api.example.com"); + assert_eq!(json["firewall_rule"]["type"], "mechanistic"); + assert_eq!(json["observation_point_id"], 2); + assert_eq!(json["is_src_dst_assignment_known"], true); + } +} diff --git a/crates/openshell-ocsf/src/events/process_activity.rs b/crates/openshell-ocsf/src/events/process_activity.rs new file mode 100644 index 00000000..204dee73 --- /dev/null +++ b/crates/openshell-ocsf/src/events/process_activity.rs @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF Process Activity [1007] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; +use crate::objects::{Actor, Process}; + +/// OCSF Process Activity Event [1007]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProcessActivityEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// The process being acted upon (required in v1.7.0). + pub process: Process, + + /// Actor (parent/supervisor process, required in v1.7.0). + #[serde(skip_serializing_if = "Option::is_none")] + pub actor: Option, + + /// Launch type ID (new in v1.7.0). + #[serde(skip_serializing_if = "Option::is_none")] + pub launch_type_id: Option, + + /// Launch type label. + #[serde(skip_serializing_if = "Option::is_none")] + pub launch_type: Option, + + /// Process exit code (for Terminate activity). + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + + /// Action ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub action_id: Option, + + /// Action label. + #[serde(skip_serializing_if = "Option::is_none")] + pub action: Option, + + /// Disposition ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition_id: Option, + + /// Disposition label. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::SeverityId; + use crate::objects::{Metadata, Product}; + + #[test] + fn test_process_activity_serialization() { + let event = ProcessActivityEvent { + base: BaseEventData::new( + 1007, + "Process Activity", + 1, + "System Activity", + 1, + "Launch", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["container".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ), + process: Process::new("python3", 42).with_cmd_line("python3 /app/main.py"), + actor: Some(Actor { + process: Process::new("openshell-sandbox", 1), + }), + launch_type_id: Some(1), + launch_type: Some("Spawn".to_string()), + exit_code: None, + action_id: Some(1), + action: Some("Allowed".to_string()), + disposition_id: Some(1), + disposition: Some("Allowed".to_string()), + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 1007); + assert_eq!(json["process"]["name"], "python3"); + assert_eq!(json["actor"]["process"]["name"], "openshell-sandbox"); + assert_eq!(json["launch_type"], "Spawn"); + } +} diff --git a/crates/openshell-ocsf/src/events/ssh_activity.rs b/crates/openshell-ocsf/src/events/ssh_activity.rs new file mode 100644 index 00000000..8071e081 --- /dev/null +++ b/crates/openshell-ocsf/src/events/ssh_activity.rs @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF SSH Activity [4007] event class. + +use serde::{Deserialize, Serialize}; + +use crate::events::base_event::BaseEventData; +use crate::objects::{Actor, Endpoint}; + +/// OCSF SSH Activity Event [4007]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SshActivityEvent { + /// Common base event fields. + #[serde(flatten)] + pub base: BaseEventData, + + /// Source endpoint (connecting peer). + #[serde(skip_serializing_if = "Option::is_none")] + pub src_endpoint: Option, + + /// Destination endpoint (SSH server). + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_endpoint: Option, + + /// Actor. + #[serde(skip_serializing_if = "Option::is_none")] + pub actor: Option, + + /// Auth type ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_type_id: Option, + + /// Auth type label. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_type: Option, + + /// SSH protocol version. + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol_ver: Option, + + /// Action ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub action_id: Option, + + /// Action label. + #[serde(skip_serializing_if = "Option::is_none")] + pub action: Option, + + /// Disposition ID. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition_id: Option, + + /// Disposition label. + #[serde(skip_serializing_if = "Option::is_none")] + pub disposition: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::{AuthTypeId, SeverityId}; + use crate::objects::{Metadata, Product}; + + #[test] + fn test_ssh_activity_serialization() { + let event = SshActivityEvent { + base: BaseEventData::new( + 4007, + "SSH Activity", + 4, + "Network Activity", + 1, + "Open", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ), + src_endpoint: Some(Endpoint::from_ip_str("10.42.0.1", 48201)), + dst_endpoint: Some(Endpoint::from_ip_str("10.42.0.2", 2222)), + actor: None, + auth_type_id: Some(AuthTypeId::Other.as_u8()), + auth_type: Some("NSSH1".to_string()), + protocol_ver: Some("NSSH1".to_string()), + action_id: Some(1), + action: Some("Allowed".to_string()), + disposition_id: Some(1), + disposition: Some("Allowed".to_string()), + }; + + let json = serde_json::to_value(&event).unwrap(); + assert_eq!(json["class_uid"], 4007); + assert_eq!(json["auth_type"], "NSSH1"); + assert_eq!(json["auth_type_id"], 99); + assert_eq!(json["protocol_ver"], "NSSH1"); + } +} diff --git a/crates/openshell-ocsf/src/format/jsonl.rs b/crates/openshell-ocsf/src/format/jsonl.rs new file mode 100644 index 00000000..59d16cc2 --- /dev/null +++ b/crates/openshell-ocsf/src/format/jsonl.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! JSONL formatter — full OCSF JSON output. + +use crate::events::OcsfEvent; + +impl OcsfEvent { + /// Serialize to a `serde_json::Value`. + /// + /// Returns the full OCSF JSON object. + #[must_use] + pub fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).expect("OcsfEvent serialization should never fail") + } + + /// Serialize as a single JSONL line (no pretty-printing, trailing newline). + #[must_use] + pub fn to_json_line(&self) -> String { + let mut line = + serde_json::to_string(self).expect("OcsfEvent serialization should never fail"); + line.push('\n'); + line + } +} + +#[cfg(test)] +mod tests { + use crate::enums::SeverityId; + use crate::events::base_event::BaseEventData; + use crate::events::{BaseEvent, OcsfEvent}; + use crate::objects::{Metadata, Product}; + + fn test_event() -> OcsfEvent { + let mut base = BaseEventData::new( + 0, + "Base Event", + 0, + "Uncategorized", + 99, + "Other", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["container".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }, + ); + base.set_time(1_742_054_400_000); + base.set_message("Test event"); + OcsfEvent::Base(BaseEvent { base }) + } + + #[test] + fn test_to_json_has_required_fields() { + let event = test_event(); + let json = event.to_json(); + + assert_eq!(json["class_uid"], 0); + assert_eq!(json["class_name"], "Base Event"); + assert_eq!(json["category_uid"], 0); + assert_eq!(json["activity_id"], 99); + assert_eq!(json["type_uid"], 99); + assert_eq!(json["time"], 1_742_054_400_000_i64); + assert_eq!(json["severity_id"], 1); + assert_eq!(json["severity"], "Informational"); + assert_eq!(json["metadata"]["version"], "1.7.0"); + } + + #[test] + fn test_to_json_line_format() { + let event = test_event(); + let line = event.to_json_line(); + + // Must be a single line ending with \n + assert!(line.ends_with('\n')); + assert_eq!(line.matches('\n').count(), 1); + + // Must parse back to the same JSON + let parsed: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); + assert_eq!(parsed, event.to_json()); + } + + #[test] + fn test_optional_fields_omitted() { + let event = test_event(); + let json = event.to_json(); + + // Optional fields should not appear when None + assert!(json.get("device").is_none()); + assert!(json.get("container").is_none()); + assert!(json.get("unmapped").is_none()); + assert!(json.get("status_detail").is_none()); + } +} diff --git a/crates/openshell-ocsf/src/format/mod.rs b/crates/openshell-ocsf/src/format/mod.rs new file mode 100644 index 00000000..084a013d --- /dev/null +++ b/crates/openshell-ocsf/src/format/mod.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF event formatters: shorthand (human-readable) and JSONL. + +pub mod jsonl; +pub mod shorthand; diff --git a/crates/openshell-ocsf/src/format/shorthand.rs b/crates/openshell-ocsf/src/format/shorthand.rs new file mode 100644 index 00000000..143a126f --- /dev/null +++ b/crates/openshell-ocsf/src/format/shorthand.rs @@ -0,0 +1,545 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Shorthand formatter — single-line human-readable format derived from OCSF events. +//! +//! Pattern: ` [context]` + +use crate::events::OcsfEvent; +use crate::objects::Url; + +/// Format a timestamp (ms since epoch) as `HH:MM:SS.mmm`. +#[must_use] +pub fn format_ts(time_ms: i64) -> String { + use chrono::{TimeZone, Utc}; + let dt = Utc.timestamp_millis_opt(time_ms).unwrap(); + dt.format("%H:%M:%S%.3f").to_string() +} + +/// Map a severity ID byte to its single-character shorthand. +#[must_use] +pub fn severity_char(severity_id: u8) -> char { + // Safe: we match on the raw u8 value + match severity_id { + 1 => 'I', + 2 => 'L', + 3 => 'M', + 4 => 'H', + 5 => 'C', + 6 => 'F', + _ => ' ', + } +} + +impl OcsfEvent { + /// Produce the single-line shorthand for `openshell.log` and gRPC log push. + /// + /// This is a display-only projection — the full OCSF JSON is the source of truth. + #[must_use] + pub fn format_shorthand(&self) -> String { + let base = self.base(); + let ts = format_ts(base.time); + let sev = severity_char(base.severity_id); + + match self { + Self::NetworkActivity(e) => { + let activity = e.base.activity_name.to_uppercase(); + let action = e.action.as_deref().unwrap_or("").to_uppercase(); + let actor_str = e + .actor + .as_ref() + .map(|a| format!("{}({})", a.process.name, a.process.pid)) + .unwrap_or_default(); + let dst = e + .dst_endpoint + .as_ref() + .map(|ep| { + let host = ep.domain_or_ip(); + let port = ep.port.map_or(String::new(), |p| format!(":{p}")); + // Include protocol for bypass detection events + let proto = e + .connection_info + .as_ref() + .map(|c| format!("/{}", c.protocol_name)) + .unwrap_or_default(); + format!("{host}{port}{proto}") + }) + .unwrap_or_default(); + let rule_ctx = e + .firewall_rule + .as_ref() + .map(|r| format!(" [policy:{} engine:{}]", r.name, r.rule_type)) + .unwrap_or_default(); + let arrow = if actor_str.is_empty() && dst.is_empty() { + String::new() + } else if actor_str.is_empty() { + format!(" {dst}") + } else if dst.is_empty() { + format!(" {actor_str}") + } else { + format!(" {actor_str} -> {dst}") + }; + + format!("{ts} {sev} NET:{activity} {action}{arrow}{rule_ctx}") + } + + Self::HttpActivity(e) => { + let method = e + .http_request + .as_ref() + .map_or("UNKNOWN", |r| r.http_method.as_str()); + let action = e.action.as_deref().unwrap_or("").to_uppercase(); + let actor_str = e + .actor + .as_ref() + .map(|a| format!("{}({})", a.process.name, a.process.pid)) + .unwrap_or_default(); + let url_str = e + .http_request + .as_ref() + .and_then(|r| r.url.as_ref()) + .map(Url::to_display_string) + .unwrap_or_default(); + let rule_ctx = e + .firewall_rule + .as_ref() + .map(|r| format!(" [policy:{}]", r.name)) + .unwrap_or_default(); + let arrow = if actor_str.is_empty() { + format!(" {method} {url_str}") + } else { + format!(" {actor_str} -> {method} {url_str}") + }; + + format!("{ts} {sev} HTTP:{method} {action}{arrow}{rule_ctx}") + } + + Self::SshActivity(e) => { + let activity = e.base.activity_name.to_uppercase(); + let action = e.action.as_deref().unwrap_or("").to_uppercase(); + let peer = e + .src_endpoint + .as_ref() + .map(|ep| { + let host = ep.domain_or_ip(); + let port = ep.port.map_or(String::new(), |p| format!(":{p}")); + format!("{host}{port}") + }) + .unwrap_or_default(); + let auth_ctx = e + .auth_type + .as_ref() + .map(|a| format!(" [auth:{a}]")) + .unwrap_or_default(); + + format!("{ts} {sev} SSH:{activity} {action} {peer}{auth_ctx}") + } + + Self::ProcessActivity(e) => { + let activity = e.base.activity_name.to_uppercase(); + let proc_str = format!("{}({})", e.process.name, e.process.pid); + let exit_ctx = e + .exit_code + .map(|c| format!(" [exit:{c}]")) + .unwrap_or_default(); + let cmd_ctx = e + .process + .cmd_line + .as_ref() + .map(|c| format!(" [cmd:{c}]")) + .unwrap_or_default(); + + format!("{ts} {sev} PROC:{activity} {proc_str}{exit_ctx}{cmd_ctx}") + } + + Self::DetectionFinding(e) => { + let disposition = e.disposition.as_deref().unwrap_or("UNKNOWN").to_uppercase(); + let title = &e.finding_info.title; + let confidence_ctx = e + .confidence + .as_ref() + .map(|c| format!(" [confidence:{}]", c.to_lowercase())) + .unwrap_or_default(); + + format!("{ts} {sev} FINDING:{disposition} \"{title}\"{confidence_ctx}") + } + + Self::ApplicationLifecycle(e) => { + let activity = e.base.activity_name.to_uppercase(); + let app = &e.app.name; + let status = e.base.status.as_deref().unwrap_or("").to_lowercase(); + + format!("{ts} {sev} LIFECYCLE:{activity} {app} {status}") + } + + Self::DeviceConfigStateChange(e) => { + let state = e.state.as_deref().unwrap_or("UNKNOWN").to_uppercase(); + let what = e.base.message.as_deref().unwrap_or("config"); + let version_ctx = e + .base + .unmapped + .as_ref() + .and_then(|u| { + let ver = u.get("policy_version").and_then(|v| v.as_str()); + let hash = u.get("policy_hash").and_then(|v| v.as_str()); + match (ver, hash) { + (Some(v), Some(h)) => Some(format!(" [version:{v} hash:{h}]")), + (Some(v), None) => Some(format!(" [version:{v}]")), + _ => None, + } + }) + .unwrap_or_default(); + + format!("{ts} {sev} CONFIG:{state} {what}{version_ctx}") + } + + Self::Base(e) => { + let message = e.base.message.as_deref().unwrap_or(""); + let unmapped_ctx = e + .base + .unmapped + .as_ref() + .and_then(|u| { + let obj = u.as_object()?; + if obj.is_empty() { + return None; + } + let fields: Vec = obj + .iter() + .take(3) // Limit to 3 most important fields + .map(|(k, v)| { + let val = v.as_str().map_or_else(|| v.to_string(), String::from); + format!("{k}:{val}") + }) + .collect(); + Some(format!(" [{}]", fields.join(" "))) + }) + .unwrap_or_default(); + + format!("{ts} {sev} EVENT {message}{unmapped_ctx}") + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::{ActionId, DispositionId}; + use crate::events::base_event::BaseEventData; + use crate::events::{ + ApplicationLifecycleEvent, BaseEvent, DetectionFindingEvent, DeviceConfigStateChangeEvent, + HttpActivityEvent, NetworkActivityEvent, ProcessActivityEvent, SshActivityEvent, + }; + use crate::objects::*; + + fn test_metadata() -> Metadata { + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + } + } + + fn base( + class_uid: u32, + class_name: &str, + cat_uid: u8, + cat_name: &str, + act_id: u8, + act_name: &str, + ) -> BaseEventData { + let mut b = BaseEventData::new( + class_uid, + class_name, + cat_uid, + cat_name, + act_id, + act_name, + crate::enums::SeverityId::Informational, + test_metadata(), + ); + b.set_time(1_742_047_200_000); // Fixed timestamp for deterministic tests + b + } + + #[test] + fn test_format_ts() { + let ts = format_ts(1_742_047_200_000); + assert_eq!(ts, "14:00:00.000"); + } + + #[test] + fn test_severity_char_mapping() { + assert_eq!(severity_char(0), ' '); + assert_eq!(severity_char(1), 'I'); + assert_eq!(severity_char(2), 'L'); + assert_eq!(severity_char(3), 'M'); + assert_eq!(severity_char(4), 'H'); + assert_eq!(severity_char(5), 'C'); + assert_eq!(severity_char(6), 'F'); + } + + #[test] + fn test_network_activity_shorthand_allow() { + let event = OcsfEvent::NetworkActivity(NetworkActivityEvent { + base: base(4001, "Network Activity", 4, "Network Activity", 1, "Open"), + src_endpoint: None, + dst_endpoint: Some(Endpoint::from_domain("api.example.com", 443)), + proxy_endpoint: None, + actor: Some(Actor { + process: Process::new("python3", 42), + }), + firewall_rule: Some(FirewallRule::new("default-egress", "mechanistic")), + connection_info: None, + action_id: Some(ActionId::Allowed.as_u8()), + action: Some("Allowed".to_string()), + disposition_id: Some(DispositionId::Allowed.as_u8()), + disposition: Some("Allowed".to_string()), + observation_point_id: None, + is_src_dst_assignment_known: None, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I NET:OPEN ALLOWED python3(42) -> api.example.com:443 [policy:default-egress engine:mechanistic]" + ); + } + + #[test] + fn test_network_activity_shorthand_bypass() { + let event = OcsfEvent::NetworkActivity(NetworkActivityEvent { + base: { + let mut b = base(4001, "Network Activity", 4, "Network Activity", 5, "Refuse"); + b.severity_id = 3; + b.severity = "Medium".to_string(); + b + }, + src_endpoint: None, + dst_endpoint: Some(Endpoint::from_ip_str("93.184.216.34", 443)), + proxy_endpoint: None, + actor: Some(Actor { + process: Process::new("node", 1234), + }), + firewall_rule: Some(FirewallRule::new("bypass-detect", "iptables")), + connection_info: Some(ConnectionInfo::new("tcp")), + action_id: Some(ActionId::Denied.as_u8()), + action: Some("Denied".to_string()), + disposition_id: Some(DispositionId::Blocked.as_u8()), + disposition: Some("Blocked".to_string()), + observation_point_id: Some(3), + is_src_dst_assignment_known: Some(true), + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 M NET:REFUSE DENIED node(1234) -> 93.184.216.34:443/tcp [policy:bypass-detect engine:iptables]" + ); + } + + #[test] + fn test_http_activity_shorthand() { + let event = OcsfEvent::HttpActivity(HttpActivityEvent { + base: base(4002, "HTTP Activity", 4, "Network Activity", 3, "Get"), + http_request: Some(HttpRequest::new( + "GET", + Url::new("https", "api.example.com", "/v1/data", 443), + )), + http_response: None, + src_endpoint: None, + dst_endpoint: None, + proxy_endpoint: None, + actor: Some(Actor { + process: Process::new("curl", 88), + }), + firewall_rule: Some(FirewallRule::new("default-egress", "mechanistic")), + action_id: Some(ActionId::Allowed.as_u8()), + action: Some("Allowed".to_string()), + disposition_id: None, + disposition: None, + observation_point_id: None, + is_src_dst_assignment_known: None, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I HTTP:GET ALLOWED curl(88) -> GET https://api.example.com/v1/data [policy:default-egress]" + ); + } + + #[test] + fn test_ssh_activity_shorthand() { + let event = OcsfEvent::SshActivity(SshActivityEvent { + base: base(4007, "SSH Activity", 4, "Network Activity", 1, "Open"), + src_endpoint: Some(Endpoint::from_ip_str("10.42.0.1", 48201)), + dst_endpoint: None, + actor: None, + auth_type_id: Some(99), + auth_type: Some("NSSH1".to_string()), + protocol_ver: None, + action_id: Some(ActionId::Allowed.as_u8()), + action: Some("Allowed".to_string()), + disposition_id: None, + disposition: None, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I SSH:OPEN ALLOWED 10.42.0.1:48201 [auth:NSSH1]" + ); + } + + #[test] + fn test_process_activity_shorthand_launch() { + let event = OcsfEvent::ProcessActivity(ProcessActivityEvent { + base: base(1007, "Process Activity", 1, "System Activity", 1, "Launch"), + process: Process::new("python3", 42).with_cmd_line("python3 /app/main.py"), + actor: None, + launch_type_id: Some(1), + launch_type: Some("Spawn".to_string()), + exit_code: None, + action_id: None, + action: None, + disposition_id: None, + disposition: None, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I PROC:LAUNCH python3(42) [cmd:python3 /app/main.py]" + ); + } + + #[test] + fn test_process_activity_shorthand_terminate() { + let event = OcsfEvent::ProcessActivity(ProcessActivityEvent { + base: base( + 1007, + "Process Activity", + 1, + "System Activity", + 2, + "Terminate", + ), + process: Process::new("python3", 42), + actor: None, + launch_type_id: None, + launch_type: None, + exit_code: Some(0), + action_id: None, + action: None, + disposition_id: None, + disposition: None, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I PROC:TERMINATE python3(42) [exit:0]" + ); + } + + #[test] + fn test_detection_finding_shorthand() { + let event = OcsfEvent::DetectionFinding(DetectionFindingEvent { + base: { + let mut b = base(2004, "Detection Finding", 2, "Findings", 1, "Create"); + b.severity_id = 4; + b.severity = "High".to_string(); + b + }, + finding_info: FindingInfo::new("nssh1-replay-abc", "NSSH1 Nonce Replay Attack"), + evidences: None, + attacks: None, + remediation: None, + is_alert: Some(true), + confidence_id: Some(3), + confidence: Some("High".to_string()), + risk_level_id: None, + risk_level: None, + action_id: None, + action: None, + disposition_id: Some(2), + disposition: Some("Blocked".to_string()), + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 H FINDING:BLOCKED \"NSSH1 Nonce Replay Attack\" [confidence:high]" + ); + } + + #[test] + fn test_lifecycle_shorthand() { + let mut b = base( + 6002, + "Application Lifecycle", + 6, + "Application Activity", + 3, + "Start", + ); + b.set_status(crate::enums::StatusId::Success); + let event = OcsfEvent::ApplicationLifecycle(ApplicationLifecycleEvent { + base: b, + app: Product { + name: "openshell-sandbox".to_string(), + vendor_name: "OpenShell".to_string(), + version: Some("0.1.0".to_string()), + }, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I LIFECYCLE:START openshell-sandbox success" + ); + } + + #[test] + fn test_config_state_change_shorthand() { + let mut b = base(5019, "Device Config State Change", 5, "Discovery", 1, "Log"); + b.set_message("policy reloaded"); + b.add_unmapped("policy_version", serde_json::json!("v3")); + b.add_unmapped("policy_hash", serde_json::json!("sha256:abc123def456")); + + let event = OcsfEvent::DeviceConfigStateChange(DeviceConfigStateChangeEvent { + base: b, + state_id: Some(2), + state: Some("LOADED".to_string()), + security_level_id: None, + security_level: None, + prev_security_level_id: None, + prev_security_level: None, + }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I CONFIG:LOADED policy reloaded [version:v3 hash:sha256:abc123def456]" + ); + } + + #[test] + fn test_base_event_shorthand() { + let mut b = base(0, "Base Event", 0, "Uncategorized", 99, "Other"); + b.set_message("Network namespace created"); + b.add_unmapped("ns", serde_json::json!("openshell-sandbox-abc123")); + + let event = OcsfEvent::Base(BaseEvent { base: b }); + + let shorthand = event.format_shorthand(); + assert_eq!( + shorthand, + "14:00:00.000 I EVENT Network namespace created [ns:openshell-sandbox-abc123]" + ); + } +} diff --git a/crates/openshell-ocsf/src/lib.rs b/crates/openshell-ocsf/src/lib.rs new file mode 100644 index 00000000..391b17c1 --- /dev/null +++ b/crates/openshell-ocsf/src/lib.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! # openshell-ocsf +//! +//! OCSF v1.7.0 event types, formatters, and tracing layers for `OpenShell` +//! sandbox logging. +//! +//! This crate provides: +//! - **8 OCSF event classes**: Network Activity, HTTP Activity, SSH Activity, +//! Process Activity, Detection Finding, Application Lifecycle, Device Config +//! State Change, and Base Event +//! - **Typed enums and objects**: All OCSF enum and object types used by the +//! event classes +//! - **Builders**: Ergonomic per-class builders with `SandboxContext` for shared +//! metadata +//! - **Dual formatters**: `format_shorthand()` for human-readable single-line +//! output, and `to_json()`/`to_json_line()` for OCSF-compliant JSONL +//! - **Tracing layers**: `OcsfShorthandLayer` and `OcsfJsonlLayer` for +//! subscriber integration +//! - **`ocsf_emit!` macro**: Thin wrapper for emitting events through the +//! tracing system + +/// OCSF schema version this crate implements. +pub const OCSF_VERSION: &str = "1.7.0"; + +pub mod builders; +pub mod enums; +pub mod events; +pub mod format; +pub mod objects; +pub mod tracing_layers; + +#[cfg(test)] +pub mod validation; + +// --- Core event types --- +pub use events::{ + ApplicationLifecycleEvent, BaseEvent, BaseEventData, DetectionFindingEvent, + DeviceConfigStateChangeEvent, HttpActivityEvent, NetworkActivityEvent, OcsfEvent, + ProcessActivityEvent, SshActivityEvent, +}; + +// --- Enum types --- +pub use enums::{ + ActionId, ActivityId, AuthTypeId, ConfidenceId, DispositionId, LaunchTypeId, RiskLevelId, + SecurityLevelId, SeverityId, StateId, StatusId, +}; + +// --- Object types --- +pub use objects::{ + Actor, Attack, ConnectionInfo, Container, Device, Endpoint, Evidence, FindingInfo, + FirewallRule, HttpRequest, HttpResponse, Image, Metadata, OsInfo, Process, Product, + Remediation, Tactic, Technique, Url, +}; + +// --- Builders --- +pub use builders::{ + AppLifecycleBuilder, BaseEventBuilder, ConfigStateChangeBuilder, DetectionFindingBuilder, + HttpActivityBuilder, NetworkActivityBuilder, ProcessActivityBuilder, SandboxContext, + SshActivityBuilder, +}; + +// --- Tracing layers --- +pub use tracing_layers::{OcsfJsonlLayer, OcsfShorthandLayer, emit_ocsf_event}; diff --git a/crates/openshell-ocsf/src/objects/attack.rs b/crates/openshell-ocsf/src/objects/attack.rs new file mode 100644 index 00000000..fce2a34b --- /dev/null +++ b/crates/openshell-ocsf/src/objects/attack.rs @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `attack`, `technique`, and `tactic` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF Attack object — MITRE ATT&CK mapping. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Attack { + /// The attack technique. + pub technique: Technique, + + /// The attack tactic. + #[serde(skip_serializing_if = "Option::is_none")] + pub tactic: Option, +} + +/// OCSF Technique object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Technique { + /// Technique ID (e.g., "T1550"). + pub uid: String, + + /// Technique name. + pub name: String, +} + +/// OCSF Tactic object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Tactic { + /// Tactic ID (e.g., "TA0008"). + pub uid: String, + + /// Tactic name. + pub name: String, +} + +impl Attack { + /// Create a MITRE ATT&CK mapping with technique and tactic. + #[must_use] + pub fn mitre( + technique_uid: &str, + technique_name: &str, + tactic_uid: &str, + tactic_name: &str, + ) -> Self { + Self { + technique: Technique { + uid: technique_uid.to_string(), + name: technique_name.to_string(), + }, + tactic: Some(Tactic { + uid: tactic_uid.to_string(), + name: tactic_name.to_string(), + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_attack_mitre() { + let attack = Attack::mitre( + "T1550", + "Use Alternate Authentication Material", + "TA0008", + "Lateral Movement", + ); + let json = serde_json::to_value(&attack).unwrap(); + assert_eq!(json["technique"]["uid"], "T1550"); + assert_eq!( + json["technique"]["name"], + "Use Alternate Authentication Material" + ); + assert_eq!(json["tactic"]["uid"], "TA0008"); + assert_eq!(json["tactic"]["name"], "Lateral Movement"); + } +} diff --git a/crates/openshell-ocsf/src/objects/connection.rs b/crates/openshell-ocsf/src/objects/connection.rs new file mode 100644 index 00000000..33d6450f --- /dev/null +++ b/crates/openshell-ocsf/src/objects/connection.rs @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `connection_info` object. + +use serde::{Deserialize, Serialize}; + +/// OCSF Connection Info object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ConnectionInfo { + /// Protocol name (e.g., "tcp", "udp"). + pub protocol_name: String, +} + +impl ConnectionInfo { + /// Create connection info with the given protocol. + #[must_use] + pub fn new(protocol: &str) -> Self { + Self { + protocol_name: protocol.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_connection_info() { + let info = ConnectionInfo::new("tcp"); + let json = serde_json::to_value(&info).unwrap(); + assert_eq!(json["protocol_name"], "tcp"); + } +} diff --git a/crates/openshell-ocsf/src/objects/container.rs b/crates/openshell-ocsf/src/objects/container.rs new file mode 100644 index 00000000..7d552245 --- /dev/null +++ b/crates/openshell-ocsf/src/objects/container.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `container` and `image` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF Container object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Container { + /// Container name. + pub name: String, + + /// 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)] +mod tests { + use super::*; + + #[test] + fn test_container_serialization() { + 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] + fn test_container_minimal() { + 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 new file mode 100644 index 00000000..4c42fb4a --- /dev/null +++ b/crates/openshell-ocsf/src/objects/device.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `device` and `os` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF Device object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Device { + /// Device hostname. + pub hostname: String, + + /// Operating system info. + #[serde(skip_serializing_if = "Option::is_none")] + pub os: Option, +} + +/// OCSF OS Info object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct OsInfo { + /// OS name (e.g., "Linux"). + pub name: String, +} + +impl Device { + /// Create a Linux device with the given hostname. + #[must_use] + pub fn linux(hostname: &str) -> Self { + Self { + hostname: hostname.to_string(), + os: Some(OsInfo { + name: "Linux".to_string(), + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_device_linux() { + let device = Device::linux("sandbox-abc123"); + let json = serde_json::to_value(&device).unwrap(); + assert_eq!(json["hostname"], "sandbox-abc123"); + assert_eq!(json["os"]["name"], "Linux"); + } +} diff --git a/crates/openshell-ocsf/src/objects/endpoint.rs b/crates/openshell-ocsf/src/objects/endpoint.rs new file mode 100644 index 00000000..3f6aae8c --- /dev/null +++ b/crates/openshell-ocsf/src/objects/endpoint.rs @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `network_endpoint` object. + +use std::net::IpAddr; + +use serde::{Deserialize, Serialize}; + +/// OCSF Network Endpoint object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Endpoint { + /// Domain name (e.g., "api.example.com"). + #[serde(skip_serializing_if = "Option::is_none")] + pub domain: Option, + + /// IP address. + #[serde(skip_serializing_if = "Option::is_none")] + pub ip: Option, + + /// Port number. + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, +} + +impl Endpoint { + /// Create an endpoint from a domain name and port. + #[must_use] + pub fn from_domain(name: &str, port: u16) -> Self { + Self { + domain: Some(name.to_string()), + ip: None, + port: Some(port), + } + } + + /// Create an endpoint from an IP address and port. + #[must_use] + pub fn from_ip(addr: IpAddr, port: u16) -> Self { + Self { + domain: None, + ip: Some(addr.to_string()), + port: Some(port), + } + } + + /// Create an endpoint from an IP string and port. + #[must_use] + pub fn from_ip_str(addr: &str, port: u16) -> Self { + Self { + domain: None, + ip: Some(addr.to_string()), + port: Some(port), + } + } + + /// Returns the domain or IP for display purposes. + #[must_use] + pub fn domain_or_ip(&self) -> &str { + self.domain + .as_deref() + .or(self.ip.as_deref()) + .unwrap_or("unknown") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_endpoint_domain() { + let ep = Endpoint::from_domain("api.example.com", 443); + assert_eq!(ep.domain_or_ip(), "api.example.com"); + let json = serde_json::to_value(&ep).unwrap(); + assert_eq!(json["domain"], "api.example.com"); + assert_eq!(json["port"], 443); + assert!(json.get("ip").is_none()); + } + + #[test] + fn test_endpoint_ip() { + let ep = Endpoint::from_ip("10.42.0.1".parse().unwrap(), 3128); + assert_eq!(ep.domain_or_ip(), "10.42.0.1"); + let json = serde_json::to_value(&ep).unwrap(); + assert_eq!(json["ip"], "10.42.0.1"); + assert!(json.get("domain").is_none()); + } +} diff --git a/crates/openshell-ocsf/src/objects/finding.rs b/crates/openshell-ocsf/src/objects/finding.rs new file mode 100644 index 00000000..5b0558b5 --- /dev/null +++ b/crates/openshell-ocsf/src/objects/finding.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `finding_info`, `evidence`, and `remediation` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF Finding Info object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FindingInfo { + /// Unique finding identifier. + pub uid: String, + + /// Finding title (short human-readable name). + pub title: String, + + /// Finding description. + #[serde(skip_serializing_if = "Option::is_none")] + pub desc: Option, +} + +impl FindingInfo { + /// Create a new finding info. + #[must_use] + pub fn new(uid: &str, title: &str) -> Self { + Self { + uid: uid.to_string(), + title: title.to_string(), + desc: None, + } + } + + /// Set the description. + #[must_use] + pub fn with_desc(mut self, desc: &str) -> Self { + self.desc = Some(desc.to_string()); + self + } +} + +/// OCSF Evidence object — artifacts associated with a finding. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Evidence { + /// Evidence data as key-value pairs. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +impl Evidence { + /// Create evidence from a map of key-value pairs. + #[must_use] + pub fn from_pairs(pairs: &[(&str, &str)]) -> Self { + let mut map = serde_json::Map::new(); + for (key, value) in pairs { + map.insert( + (*key).to_string(), + serde_json::Value::String((*value).to_string()), + ); + } + Self { + data: Some(serde_json::Value::Object(map)), + } + } +} + +/// OCSF Remediation object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Remediation { + /// Remediation description / guidance. + pub desc: String, +} + +impl Remediation { + /// Create a new remediation. + #[must_use] + pub fn new(desc: &str) -> Self { + Self { + desc: desc.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_finding_info_creation() { + let info = FindingInfo::new("nssh1-replay-abc", "NSSH1 Nonce Replay Attack") + .with_desc("A nonce was replayed."); + let json = serde_json::to_value(&info).unwrap(); + assert_eq!(json["uid"], "nssh1-replay-abc"); + assert_eq!(json["title"], "NSSH1 Nonce Replay Attack"); + assert_eq!(json["desc"], "A nonce was replayed."); + } + + #[test] + fn test_evidence_from_pairs() { + let evidence = Evidence::from_pairs(&[("nonce", "0xdeadbeef"), ("peer_ip", "10.42.0.1")]); + let json = serde_json::to_value(&evidence).unwrap(); + assert_eq!(json["data"]["nonce"], "0xdeadbeef"); + assert_eq!(json["data"]["peer_ip"], "10.42.0.1"); + } + + #[test] + fn test_remediation() { + let rem = Remediation::new("Set NODE_USE_ENV_PROXY=1"); + let json = serde_json::to_value(&rem).unwrap(); + assert_eq!(json["desc"], "Set NODE_USE_ENV_PROXY=1"); + } +} diff --git a/crates/openshell-ocsf/src/objects/firewall_rule.rs b/crates/openshell-ocsf/src/objects/firewall_rule.rs new file mode 100644 index 00000000..89909a86 --- /dev/null +++ b/crates/openshell-ocsf/src/objects/firewall_rule.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `firewall_rule` object. + +use serde::{Deserialize, Serialize}; + +/// OCSF Firewall Rule object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FirewallRule { + /// Rule name (e.g., "default-egress", "bypass-detect"). + pub name: String, + + /// Rule type / engine (e.g., "mechanistic", "opa", "iptables"). + #[serde(rename = "type")] + pub rule_type: String, +} + +impl FirewallRule { + /// Create a new firewall rule. + #[must_use] + pub fn new(name: &str, rule_type: &str) -> Self { + Self { + name: name.to_string(), + rule_type: rule_type.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_firewall_rule_serialization() { + let rule = FirewallRule::new("default-egress", "mechanistic"); + let json = serde_json::to_value(&rule).unwrap(); + assert_eq!(json["name"], "default-egress"); + assert_eq!(json["type"], "mechanistic"); + } +} diff --git a/crates/openshell-ocsf/src/objects/http.rs b/crates/openshell-ocsf/src/objects/http.rs new file mode 100644 index 00000000..6ed099b1 --- /dev/null +++ b/crates/openshell-ocsf/src/objects/http.rs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `http_request`, `http_response`, and `url` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF HTTP Request object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HttpRequest { + /// HTTP method (e.g., "GET", "POST"). + pub http_method: String, + + /// Request URL. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + +/// OCSF HTTP Response object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HttpResponse { + /// HTTP status code. + pub code: u16, +} + +/// OCSF URL object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Url { + /// URL scheme (e.g., "https"). + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme: Option, + + /// Hostname. + #[serde(skip_serializing_if = "Option::is_none")] + pub hostname: Option, + + /// URL path. + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// Port number. + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, +} + +impl Url { + /// Create a URL from its components. + #[must_use] + pub fn new(scheme: &str, hostname: &str, path: &str, port: u16) -> Self { + Self { + scheme: Some(scheme.to_string()), + hostname: Some(hostname.to_string()), + path: Some(path.to_string()), + port: Some(port), + } + } + + /// Format as a display string. + #[must_use] + pub fn to_display_string(&self) -> String { + let scheme = self.scheme.as_deref().unwrap_or("https"); + let hostname = self.hostname.as_deref().unwrap_or("unknown"); + let path = self.path.as_deref().unwrap_or("/"); + format!("{scheme}://{hostname}{path}") + } +} + +impl HttpRequest { + /// Create a new HTTP request. + #[must_use] + pub fn new(method: &str, url: Url) -> Self { + Self { + http_method: method.to_string(), + url: Some(url), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_http_request_serialization() { + let req = HttpRequest::new("GET", Url::new("https", "api.example.com", "/v1/data", 443)); + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["http_method"], "GET"); + assert_eq!(json["url"]["scheme"], "https"); + assert_eq!(json["url"]["hostname"], "api.example.com"); + assert_eq!(json["url"]["path"], "/v1/data"); + assert_eq!(json["url"]["port"], 443); + } + + #[test] + fn test_url_display_string() { + let url = Url::new("https", "api.example.com", "/v1/data", 443); + assert_eq!(url.to_display_string(), "https://api.example.com/v1/data"); + } + + #[test] + fn test_http_response() { + let resp = HttpResponse { code: 200 }; + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["code"], 200); + } +} diff --git a/crates/openshell-ocsf/src/objects/metadata.rs b/crates/openshell-ocsf/src/objects/metadata.rs new file mode 100644 index 00000000..0ffc8e15 --- /dev/null +++ b/crates/openshell-ocsf/src/objects/metadata.rs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `metadata` and `product` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF Metadata object — event provenance and schema info. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Metadata { + /// OCSF schema version (e.g., "1.7.0"). + pub version: String, + + /// The product that generated the event. + pub product: Product, + + /// OCSF profiles applied to this event. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub profiles: Vec, + + /// Unique event source identifier (sandbox ID). + #[serde(skip_serializing_if = "Option::is_none")] + pub uid: Option, + + /// Log source path (e.g., "/dev/kmsg" for bypass detection). + #[serde(skip_serializing_if = "Option::is_none")] + pub log_source: Option, +} + +/// OCSF Product object — identifies the software generating events. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Product { + /// Product name. + pub name: String, + + /// Vendor name. + pub vendor_name: String, + + /// Product version. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +impl Product { + /// Create the standard `OpenShell` Sandbox Supervisor product. + #[must_use] + pub fn openshell_sandbox(version: &str) -> Self { + Self { + name: "OpenShell Sandbox Supervisor".to_string(), + vendor_name: "OpenShell".to_string(), + version: Some(version.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metadata_serialization() { + let metadata = Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec!["security_control".to_string(), "container".to_string()], + uid: Some("sandbox-abc123".to_string()), + log_source: None, + }; + + let json = serde_json::to_value(&metadata).unwrap(); + assert_eq!(json["version"], "1.7.0"); + assert_eq!(json["product"]["name"], "OpenShell Sandbox Supervisor"); + assert_eq!(json["product"]["vendor_name"], "OpenShell"); + assert!(json.get("log_source").is_none()); + } + + #[test] + fn test_metadata_with_log_source() { + let metadata = Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec![], + uid: None, + log_source: Some("/dev/kmsg".to_string()), + }; + + let json = serde_json::to_value(&metadata).unwrap(); + assert_eq!(json["log_source"], "/dev/kmsg"); + // Empty profiles should be omitted + assert!(json.get("profiles").is_none()); + } +} diff --git a/crates/openshell-ocsf/src/objects/mod.rs b/crates/openshell-ocsf/src/objects/mod.rs new file mode 100644 index 00000000..8493c19d --- /dev/null +++ b/crates/openshell-ocsf/src/objects/mod.rs @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF v1.7.0 object types. + +mod attack; +mod connection; +mod container; +mod device; +mod endpoint; +mod finding; +mod firewall_rule; +mod http; +mod metadata; +mod process; + +pub use attack::{Attack, Tactic, Technique}; +pub use connection::ConnectionInfo; +pub use container::{Container, Image}; +pub use device::{Device, OsInfo}; +pub use endpoint::Endpoint; +pub use finding::{Evidence, FindingInfo, Remediation}; +pub use firewall_rule::FirewallRule; +pub use http::{HttpRequest, HttpResponse, Url}; +pub use metadata::{Metadata, Product}; +pub use process::{Actor, Process}; diff --git a/crates/openshell-ocsf/src/objects/process.rs b/crates/openshell-ocsf/src/objects/process.rs new file mode 100644 index 00000000..216fba20 --- /dev/null +++ b/crates/openshell-ocsf/src/objects/process.rs @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `process` and `actor` objects. + +use serde::{Deserialize, Serialize}; + +/// OCSF Process object. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Process { + /// Process name (e.g., "python3"). + pub name: String, + + /// Process ID. + pub pid: i64, + + /// Full command line. + #[serde(skip_serializing_if = "Option::is_none")] + pub cmd_line: Option, + + /// Parent process. + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_process: Option>, +} + +impl Process { + /// Create a new process with name and PID. + #[must_use] + pub fn new(name: &str, pid: i64) -> Self { + Self { + name: name.to_string(), + pid, + cmd_line: None, + parent_process: None, + } + } + + /// Set the command line. + #[must_use] + pub fn with_cmd_line(mut self, cmd_line: &str) -> Self { + self.cmd_line = Some(cmd_line.to_string()); + self + } + + /// Set the parent process. + #[must_use] + pub fn with_parent(mut self, parent: Self) -> Self { + self.parent_process = Some(Box::new(parent)); + self + } + + /// Build a process chain from bypass detection fields. + /// + /// Parses an ancestors string like "bash -> node" into a parent chain. + #[must_use] + pub fn from_bypass(binary: &str, pid: &str, ancestors: &str) -> Self { + let pid = pid.parse::().unwrap_or(0); + let mut proc = Self::new(binary, pid); + + // Parse ancestor chain "grandparent -> parent" into nested parent_process + let parts: Vec<&str> = ancestors.split(" -> ").collect(); + if parts.len() >= 2 { + let mut parent = Self::new(parts[parts.len() - 1], 0); + for ancestor in parts[..parts.len() - 1].iter().rev() { + parent = Self::new(ancestor, 0).with_parent(parent); + } + proc.parent_process = Some(Box::new(parent)); + } else if !ancestors.is_empty() && ancestors != binary { + proc.parent_process = Some(Box::new(Self::new(ancestors, 0))); + } + + proc + } +} + +/// OCSF Actor object — the entity that initiated the event. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Actor { + /// The process that performed the action. + pub process: Process, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_process_creation() { + let proc = Process::new("python3", 42).with_cmd_line("python3 /app/main.py"); + assert_eq!(proc.name, "python3"); + assert_eq!(proc.pid, 42); + assert_eq!(proc.cmd_line.as_deref(), Some("python3 /app/main.py")); + } + + #[test] + fn test_process_with_parent() { + let proc = Process::new("python3", 42).with_parent(Process::new("bash", 1)); + let parent = proc.parent_process.as_ref().unwrap(); + assert_eq!(parent.name, "bash"); + assert_eq!(parent.pid, 1); + } + + #[test] + fn test_process_from_bypass() { + let proc = Process::from_bypass("node", "1234", "bash -> node"); + assert_eq!(proc.name, "node"); + assert_eq!(proc.pid, 1234); + let parent = proc.parent_process.as_ref().unwrap(); + assert_eq!(parent.name, "bash"); + } + + #[test] + fn test_process_from_bypass_deep_chain() { + let proc = Process::from_bypass("node", "1234", "init -> bash -> node"); + assert_eq!(proc.name, "node"); + let parent = proc.parent_process.as_ref().unwrap(); + assert_eq!(parent.name, "init"); + let grandparent = parent.parent_process.as_ref().unwrap(); + assert_eq!(grandparent.name, "bash"); + } + + #[test] + fn test_process_serialization() { + let proc = Process::new("python3", 42); + let json = serde_json::to_value(&proc).unwrap(); + assert_eq!(json["name"], "python3"); + assert_eq!(json["pid"], 42); + assert!(json.get("cmd_line").is_none()); + assert!(json.get("parent_process").is_none()); + } + + #[test] + fn test_actor_serialization() { + let actor = Actor { + process: Process::new("python3", 42), + }; + let json = serde_json::to_value(&actor).unwrap(); + assert_eq!(json["process"]["name"], "python3"); + } +} diff --git a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs new file mode 100644 index 00000000..dad9c10b --- /dev/null +++ b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Bridge between `OcsfEvent` structs and the tracing system. +//! +//! The `emit_ocsf_event` function serializes an `OcsfEvent` and emits it +//! as a tracing event with target `ocsf`. The custom layers intercept +//! events with this target and format them. + +use std::sync::OnceLock; + +use crate::events::OcsfEvent; + +std::thread_local! { + // Thread-local storage for the current OCSF event being emitted. + // Used by the tracing layers to retrieve the full OcsfEvent struct. + static CURRENT_EVENT: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +/// Target string used to identify OCSF tracing events. +pub static OCSF_TARGET: &str = "ocsf"; + +/// Sentinel field name on the tracing event that signals an OCSF event is available. +static _OCSF_FIELD: OnceLock<&str> = OnceLock::new(); + +/// Retrieve (and take) the current thread-local OCSF event, if any. +pub fn take_current_event() -> Option { + CURRENT_EVENT.with(|cell| cell.borrow_mut().take()) +} + +/// Emit an `OcsfEvent` through the tracing subscriber. +/// +/// The OCSF layers (`OcsfShorthandLayer`, `OcsfJsonlLayer`) format it +/// as shorthand (`openshell.log`) and JSONL (`openshell-ocsf.log`). +pub fn emit_ocsf_event(event: OcsfEvent) { + // Store the event in thread-local so layers can access it + CURRENT_EVENT.with(|cell| { + *cell.borrow_mut() = Some(event); + }); + + // Emit a tracing event with the `ocsf` target. + // The layers detect this target and pull the OcsfEvent from thread-local. + tracing::info!(target: "ocsf", "ocsf_event"); + + // Clean up if layers didn't consume it (e.g., no OCSF layers registered) + CURRENT_EVENT.with(|cell| { + cell.borrow_mut().take(); + }); +} + +/// Convenience macro for emitting an `OcsfEvent`. +/// +/// ```ignore +/// use openshell_ocsf::ocsf_emit; +/// ocsf_emit!(event); +/// ``` +#[macro_export] +macro_rules! ocsf_emit { + ($event:expr) => { + $crate::tracing_layers::emit_ocsf_event($event) + }; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::enums::SeverityId; + use crate::events::base_event::BaseEventData; + use crate::events::{BaseEvent, OcsfEvent}; + use crate::objects::{Metadata, Product}; + + #[test] + fn test_thread_local_store_and_take() { + let event = OcsfEvent::Base(BaseEvent { + base: BaseEventData::new( + 0, + "Base Event", + 0, + "Uncategorized", + 99, + "Other", + SeverityId::Informational, + Metadata { + version: "1.7.0".to_string(), + product: Product::openshell_sandbox("0.1.0"), + profiles: vec![], + uid: None, + log_source: None, + }, + ), + }); + + CURRENT_EVENT.with(|cell| { + *cell.borrow_mut() = Some(event); + }); + + let taken = take_current_event(); + assert!(taken.is_some()); + assert_eq!(taken.unwrap().class_uid(), 0); + + // Second take should be None + assert!(take_current_event().is_none()); + } +} diff --git a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs new file mode 100644 index 00000000..c96c0ced --- /dev/null +++ b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tracing layer that writes OCSF JSONL to a writer. + +use std::io::Write; +use std::sync::Mutex; + +use tracing::Subscriber; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; + +use super::event_bridge::{OCSF_TARGET, take_current_event}; + +/// A tracing `Layer` that intercepts OCSF events and writes JSONL output. +/// +/// Only events with `target: "ocsf"` are processed; non-OCSF events are ignored. +pub struct OcsfJsonlLayer { + writer: Mutex, +} + +impl OcsfJsonlLayer { + /// Create a new JSONL layer writing to the given writer. + #[must_use] + pub fn new(writer: W) -> Self { + Self { + writer: Mutex::new(writer), + } + } +} + +impl Layer for OcsfJsonlLayer +where + S: Subscriber, + W: Write + Send + 'static, +{ + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { + if event.metadata().target() != OCSF_TARGET { + return; + } + + if let Some(ocsf_event) = take_current_event() { + let line = ocsf_event.to_json_line(); + if let Ok(mut w) = self.writer.lock() { + let _ = w.write_all(line.as_bytes()); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_jsonl_layer_creation() { + let buffer: Vec = Vec::new(); + let _layer = OcsfJsonlLayer::new(buffer); + } +} diff --git a/crates/openshell-ocsf/src/tracing_layers/mod.rs b/crates/openshell-ocsf/src/tracing_layers/mod.rs new file mode 100644 index 00000000..738437f0 --- /dev/null +++ b/crates/openshell-ocsf/src/tracing_layers/mod.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tracing layers for dual-file OCSF output and event bridging. +//! +//! - [`OcsfShorthandLayer`] writes human-readable shorthand to a writer +//! - [`OcsfJsonlLayer`] writes OCSF JSONL to a writer +//! - [`emit_ocsf_event`] bridges `OcsfEvent` structs into the tracing system + +mod event_bridge; +mod jsonl_layer; +mod shorthand_layer; + +pub use event_bridge::emit_ocsf_event; +pub use jsonl_layer::OcsfJsonlLayer; +pub use shorthand_layer::OcsfShorthandLayer; diff --git a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs new file mode 100644 index 00000000..488480fd --- /dev/null +++ b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tracing layer that writes OCSF shorthand to a writer. + +use std::io::Write; +use std::sync::Mutex; + +use tracing::Subscriber; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; + +use super::event_bridge::{OCSF_TARGET, take_current_event}; + +/// A tracing `Layer` that intercepts OCSF events and writes shorthand output. +/// +/// Events with `target: "ocsf"` are formatted via `format_shorthand()`. +/// Non-OCSF events are formatted with a simple fallback. +pub struct OcsfShorthandLayer { + writer: Mutex, + /// Whether to include non-OCSF events in the output. + include_non_ocsf: bool, +} + +impl OcsfShorthandLayer { + /// Create a new shorthand layer writing to the given writer. + #[must_use] + pub fn new(writer: W) -> Self { + Self { + writer: Mutex::new(writer), + include_non_ocsf: true, + } + } + + /// Set whether non-OCSF tracing events should be included. + #[must_use] + pub fn with_non_ocsf(mut self, include: bool) -> Self { + self.include_non_ocsf = include; + self + } +} + +impl Layer for OcsfShorthandLayer +where + S: Subscriber, + W: Write + Send + 'static, +{ + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { + let meta = event.metadata(); + + if meta.target() == OCSF_TARGET { + // This is an OCSF event — pull from thread-local + if let Some(ocsf_event) = take_current_event() { + let line = ocsf_event.format_shorthand(); + if let Ok(mut w) = self.writer.lock() { + let _ = writeln!(w, "{line}"); + } + } + } else if self.include_non_ocsf { + // Fallback: format non-OCSF events with basic format + let level = meta.level(); + let target = meta.target(); + // Extract message from the event fields + let mut message = String::new(); + event.record(&mut MessageVisitor(&mut message)); + if let Ok(mut w) = self.writer.lock() { + let _ = writeln!(w, "{level} {target}: {message}"); + } + } + } +} + +/// Simple visitor that extracts the message field from tracing events. +struct MessageVisitor<'a>(&'a mut String); + +impl tracing::field::Visit for MessageVisitor<'_> { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + if field.name() == "message" { + *self.0 = format!("{value:?}"); + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + *self.0 = value.to_string(); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_shorthand_layer_creation() { + let buffer: Vec = Vec::new(); + let _layer = OcsfShorthandLayer::new(buffer); + } + + #[test] + fn test_shorthand_layer_with_non_ocsf_toggle() { + let buffer: Vec = Vec::new(); + let layer = OcsfShorthandLayer::new(buffer).with_non_ocsf(false); + assert!(!layer.include_non_ocsf); + } +} diff --git a/crates/openshell-ocsf/src/validation/mod.rs b/crates/openshell-ocsf/src/validation/mod.rs new file mode 100644 index 00000000..28545337 --- /dev/null +++ b/crates/openshell-ocsf/src/validation/mod.rs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Schema validation utilities for testing OCSF events against vendored schemas. +//! +//! These utilities are gated behind `#[cfg(test)]` — they are only available +//! in test builds. + +pub mod schema; + +pub use schema::{load_class_schema, validate_enum_value, validate_required_fields}; diff --git a/crates/openshell-ocsf/src/validation/schema.rs b/crates/openshell-ocsf/src/validation/schema.rs new file mode 100644 index 00000000..f1160ecd --- /dev/null +++ b/crates/openshell-ocsf/src/validation/schema.rs @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Schema loading and validation helpers. + +use serde_json::Value; +use std::fs; + +/// Load a vendored OCSF class schema by name. +/// +/// # Panics +/// +/// Panics if the schema file is missing or contains invalid JSON. +#[must_use] +pub fn load_class_schema(class: &str) -> Value { + let path = format!( + "{}/schemas/ocsf/v1.7.0/classes/{class}.json", + env!("CARGO_MANIFEST_DIR") + ); + let data = + fs::read_to_string(&path).unwrap_or_else(|_| panic!("Missing vendored schema: {path}")); + serde_json::from_str(&data).unwrap_or_else(|e| panic!("Invalid JSON in {path}: {e}")) +} + +/// Load a vendored OCSF object schema by name. +/// +/// # Panics +/// +/// Panics if the schema file is missing or contains invalid JSON. +#[must_use] +pub fn load_object_schema(object: &str) -> Value { + let path = format!( + "{}/schemas/ocsf/v1.7.0/objects/{object}.json", + env!("CARGO_MANIFEST_DIR") + ); + let data = + fs::read_to_string(&path).unwrap_or_else(|_| panic!("Missing vendored schema: {path}")); + serde_json::from_str(&data).unwrap_or_else(|e| panic!("Invalid JSON in {path}: {e}")) +} + +/// Validate that all required fields from the schema are present in the event JSON. +/// +/// The OCSF schema stores attributes as an object where each key is a field name +/// and the value contains a `requirement` field. +pub fn validate_required_fields(event: &Value, schema: &Value) { + if let Some(attrs) = schema.get("attributes").and_then(|a| a.as_object()) { + for (name, def) in attrs { + if def.get("requirement").and_then(|r| r.as_str()) == Some("required") { + assert!( + event.get(name).is_some(), + "Missing required field '{name}' in OCSF event. Event keys: {:?}", + event.as_object().map(|o| o.keys().collect::>()) + ); + } + } + } +} + +/// Validate that an enum field in the event has a valid value per the schema. +/// +/// Checks the `enum` map in the schema attribute definition. +pub fn validate_enum_value(event: &Value, field: &str, schema: &Value) { + if let Some(val) = event.get(field) + && let Some(attrs) = schema.get("attributes").and_then(|a| a.as_object()) + && let Some(def) = attrs.get(field) + && let Some(enum_map) = def.get("enum").and_then(|e| e.as_object()) + { + let key = val.to_string(); + let key = key.trim_matches('"'); + assert!( + enum_map.contains_key(key), + "Invalid enum value {val} for field '{field}'. Valid: {:?}", + enum_map.keys().collect::>() + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_class_schemas() { + // These tests only pass when the vendored schemas are present + let classes = [ + "network_activity", + "http_activity", + "ssh_activity", + "process_activity", + "detection_finding", + "application_lifecycle", + "device_config_state_change", + "base_event", + ]; + + for class in &classes { + let schema = load_class_schema(class); + // Every class schema should have a caption and attributes + assert!( + schema.get("caption").is_some(), + "Schema '{class}' missing 'caption'" + ); + assert!( + schema.get("attributes").is_some(), + "Schema '{class}' missing 'attributes'" + ); + } + } + + #[test] + fn test_validate_required_fields_passes() { + let event = serde_json::json!({ + "class_uid": 0, + "severity_id": 1, + "metadata": {}, + "time": 12345, + "type_uid": 99, + "activity_id": 99, + "category_uid": 0 + }); + let schema = load_class_schema("base_event"); + // This should not panic — base_event has few required fields + validate_required_fields(&event, &schema); + } + + #[test] + fn test_validate_enum_value_valid() { + let event = serde_json::json!({ "severity_id": 1 }); + let schema = load_class_schema("base_event"); + validate_enum_value(&event, "severity_id", &schema); + } +} From 313d35bf328ab4df7fec4bffebdbf2812a002a84 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:12:20 -0700 Subject: [PATCH 02/11] fix(ocsf): correct ActionId enum values to match OCSF v1.7.0 schema Rename Alerted(3) to Observed and Dropped(4) to Modified per the OCSF v1.7.0 action_id specification. The previous values were incorrectly sourced from disposition_id captions. --- crates/openshell-ocsf/src/enums/action.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/openshell-ocsf/src/enums/action.rs b/crates/openshell-ocsf/src/enums/action.rs index 8a4638c1..0d5eab24 100644 --- a/crates/openshell-ocsf/src/enums/action.rs +++ b/crates/openshell-ocsf/src/enums/action.rs @@ -15,10 +15,10 @@ pub enum ActionId { Allowed = 1, /// 2 — Denied Denied = 2, - /// 3 — Alerted - Alerted = 3, - /// 4 — Dropped - Dropped = 4, + /// 3 — Observed + Observed = 3, + /// 4 — Modified + Modified = 4, /// 99 — Other Other = 99, } @@ -30,8 +30,8 @@ impl ActionId { Self::Unknown => "Unknown", Self::Allowed => "Allowed", Self::Denied => "Denied", - Self::Alerted => "Alerted", - Self::Dropped => "Dropped", + Self::Observed => "Observed", + Self::Modified => "Modified", Self::Other => "Other", } } @@ -51,8 +51,8 @@ mod tests { assert_eq!(ActionId::Unknown.label(), "Unknown"); assert_eq!(ActionId::Allowed.label(), "Allowed"); assert_eq!(ActionId::Denied.label(), "Denied"); - assert_eq!(ActionId::Alerted.label(), "Alerted"); - assert_eq!(ActionId::Dropped.label(), "Dropped"); + assert_eq!(ActionId::Observed.label(), "Observed"); + assert_eq!(ActionId::Modified.label(), "Modified"); assert_eq!(ActionId::Other.label(), "Other"); } From 716588f25285b5aca306dad55a89306d0f90ab5d Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:13:31 -0700 Subject: [PATCH 03/11] fix(ocsf): implement class_uid-based deserialization for OcsfEvent Replace #[serde(untagged)] with a custom Deserialize impl that reads class_uid first, then dispatches to the correct event variant. The untagged approach was ambiguous since all variants share flattened BaseEventData with optional fields, causing serde to always match the first variant. Add round-trip tests for all 8 event classes. --- crates/openshell-ocsf/src/events/mod.rs | 242 +++++++++++++++++++++++- 1 file changed, 240 insertions(+), 2 deletions(-) diff --git a/crates/openshell-ocsf/src/events/mod.rs b/crates/openshell-ocsf/src/events/mod.rs index 6af68c18..bd7526d7 100644 --- a/crates/openshell-ocsf/src/events/mod.rs +++ b/crates/openshell-ocsf/src/events/mod.rs @@ -21,11 +21,15 @@ pub use network_activity::NetworkActivityEvent; pub use process_activity::ProcessActivityEvent; pub use ssh_activity::SshActivityEvent; +use serde::ser::SerializeMap; use serde::{Deserialize, Serialize}; /// Top-level OCSF event enum encompassing all supported event classes. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(untagged)] +/// +/// Serialization uses the inner event struct directly (untagged). +/// Deserialization dispatches on the `class_uid` field to select the +/// correct variant, avoiding the ambiguity of `#[serde(untagged)]`. +#[derive(Debug, Clone, PartialEq, Eq)] pub enum OcsfEvent { /// Network Activity [4001] NetworkActivity(NetworkActivityEvent), @@ -45,6 +49,86 @@ pub enum OcsfEvent { Base(BaseEvent), } +impl Serialize for OcsfEvent { + fn serialize(&self, serializer: S) -> Result { + // Serialize the inner event struct directly — produces flat OCSF JSON. + // We serialize to a Value first to get the map, then re-serialize. + let value = match self { + Self::NetworkActivity(e) => { + serde_json::to_value(e).map_err(serde::ser::Error::custom)? + } + Self::HttpActivity(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, + Self::SshActivity(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, + Self::ProcessActivity(e) => { + serde_json::to_value(e).map_err(serde::ser::Error::custom)? + } + Self::DetectionFinding(e) => { + serde_json::to_value(e).map_err(serde::ser::Error::custom)? + } + Self::ApplicationLifecycle(e) => { + serde_json::to_value(e).map_err(serde::ser::Error::custom)? + } + Self::DeviceConfigStateChange(e) => { + serde_json::to_value(e).map_err(serde::ser::Error::custom)? + } + Self::Base(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, + }; + + // Re-serialize the flat JSON object + if let serde_json::Value::Object(map) = value { + let mut ser_map = serializer.serialize_map(Some(map.len()))?; + for (k, v) in &map { + ser_map.serialize_entry(k, v)?; + } + ser_map.end() + } else { + Err(serde::ser::Error::custom("expected JSON object")) + } + } +} + +impl<'de> Deserialize<'de> for OcsfEvent { + fn deserialize>(deserializer: D) -> Result { + // Deserialize into a raw JSON value first, then dispatch on class_uid. + let value = serde_json::Value::deserialize(deserializer)?; + + let class_uid = value + .get("class_uid") + .and_then(serde_json::Value::as_u64) + .ok_or_else(|| serde::de::Error::missing_field("class_uid"))?; + + match class_uid { + 4001 => serde_json::from_value::(value) + .map(Self::NetworkActivity) + .map_err(serde::de::Error::custom), + 4002 => serde_json::from_value::(value) + .map(Self::HttpActivity) + .map_err(serde::de::Error::custom), + 4007 => serde_json::from_value::(value) + .map(Self::SshActivity) + .map_err(serde::de::Error::custom), + 1007 => serde_json::from_value::(value) + .map(Self::ProcessActivity) + .map_err(serde::de::Error::custom), + 2004 => serde_json::from_value::(value) + .map(Self::DetectionFinding) + .map_err(serde::de::Error::custom), + 6002 => serde_json::from_value::(value) + .map(Self::ApplicationLifecycle) + .map_err(serde::de::Error::custom), + 5019 => serde_json::from_value::(value) + .map(Self::DeviceConfigStateChange) + .map_err(serde::de::Error::custom), + 0 => serde_json::from_value::(value) + .map(Self::Base) + .map_err(serde::de::Error::custom), + other => Err(serde::de::Error::custom(format!( + "unknown OCSF class_uid: {other}" + ))), + } + } +} + impl OcsfEvent { /// Returns the OCSF `class_uid` for this event. #[must_use] @@ -76,3 +160,157 @@ impl OcsfEvent { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::builders::{ + test_sandbox_context, AppLifecycleBuilder, BaseEventBuilder, ConfigStateChangeBuilder, + DetectionFindingBuilder, HttpActivityBuilder, NetworkActivityBuilder, + ProcessActivityBuilder, SshActivityBuilder, + }; + use crate::enums::*; + use crate::objects::*; + + /// Verify that every event class round-trips through JSON and deserializes + /// to the correct `OcsfEvent` variant (not silently matching the wrong one). + #[test] + fn test_roundtrip_network_activity() { + let ctx = test_sandbox_context(); + let event = NetworkActivityBuilder::new(&ctx) + .activity(ActivityId::Open) + .action(ActionId::Allowed) + .disposition(DispositionId::Allowed) + .severity(SeverityId::Informational) + .dst_endpoint(Endpoint::from_domain("example.com", 443)) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::NetworkActivity(_))); + assert_eq!(deserialized.class_uid(), 4001); + } + + #[test] + fn test_roundtrip_http_activity() { + let ctx = test_sandbox_context(); + let event = HttpActivityBuilder::new(&ctx) + .activity(ActivityId::Reset) + .action(ActionId::Allowed) + .severity(SeverityId::Informational) + .http_request(HttpRequest::new( + "GET", + Url::new("https", "example.com", "/", 443), + )) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::HttpActivity(_))); + assert_eq!(deserialized.class_uid(), 4002); + } + + #[test] + fn test_roundtrip_ssh_activity() { + let ctx = test_sandbox_context(); + let event = SshActivityBuilder::new(&ctx) + .activity(ActivityId::Open) + .action(ActionId::Allowed) + .severity(SeverityId::Informational) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::SshActivity(_))); + assert_eq!(deserialized.class_uid(), 4007); + } + + #[test] + fn test_roundtrip_process_activity() { + let ctx = test_sandbox_context(); + let event = ProcessActivityBuilder::new(&ctx) + .activity(ActivityId::Open) + .severity(SeverityId::Informational) + .process(Process::new("test", 1)) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::ProcessActivity(_))); + assert_eq!(deserialized.class_uid(), 1007); + } + + #[test] + fn test_roundtrip_detection_finding() { + let ctx = test_sandbox_context(); + let event = DetectionFindingBuilder::new(&ctx) + .severity(SeverityId::High) + .finding_info(FindingInfo::new("test-uid", "Test Finding")) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::DetectionFinding(_))); + assert_eq!(deserialized.class_uid(), 2004); + } + + #[test] + fn test_roundtrip_application_lifecycle() { + let ctx = test_sandbox_context(); + let event = AppLifecycleBuilder::new(&ctx) + .activity(ActivityId::Reset) + .severity(SeverityId::Informational) + .status(StatusId::Success) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::ApplicationLifecycle(_))); + assert_eq!(deserialized.class_uid(), 6002); + } + + #[test] + fn test_roundtrip_config_state_change() { + let ctx = test_sandbox_context(); + let event = ConfigStateChangeBuilder::new(&ctx) + .state(StateId::Enabled, "loaded") + .severity(SeverityId::Informational) + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!( + deserialized, + OcsfEvent::DeviceConfigStateChange(_) + )); + assert_eq!(deserialized.class_uid(), 5019); + } + + #[test] + fn test_roundtrip_base_event() { + let ctx = test_sandbox_context(); + let event = BaseEventBuilder::new(&ctx) + .severity(SeverityId::Informational) + .message("test") + .build(); + + let json = serde_json::to_value(&event).unwrap(); + let deserialized: OcsfEvent = serde_json::from_value(json).unwrap(); + assert!(matches!(deserialized, OcsfEvent::Base(_))); + assert_eq!(deserialized.class_uid(), 0); + } + + #[test] + fn test_deserialize_unknown_class_uid_errors() { + let json = serde_json::json!({"class_uid": 9999}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_missing_class_uid_errors() { + let json = serde_json::json!({"severity_id": 1}); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } +} From b3953583e08bfad9a386e3289f9d5bc786632a0e Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:14:40 -0700 Subject: [PATCH 04/11] fix(ocsf): use non-consuming clone for thread-local event bridge Replace take_current_event() with clone_current_event() so both OcsfShorthandLayer and OcsfJsonlLayer receive the event. The previous .take() approach consumed the event on the first layer call, starving the second layer in the dual-file output setup. --- .../src/tracing_layers/event_bridge.rs | 76 +++++++++++++------ .../src/tracing_layers/jsonl_layer.rs | 6 +- .../src/tracing_layers/shorthand_layer.rs | 8 +- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs index dad9c10b..caa94ca8 100644 --- a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs +++ b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs @@ -3,35 +3,37 @@ //! Bridge between `OcsfEvent` structs and the tracing system. //! -//! The `emit_ocsf_event` function serializes an `OcsfEvent` and emits it -//! as a tracing event with target `ocsf`. The custom layers intercept -//! events with this target and format them. - -use std::sync::OnceLock; +//! The `emit_ocsf_event` function stores an `OcsfEvent` in thread-local +//! storage, then emits a tracing event with target `ocsf`. The custom +//! layers intercept this target, clone the event, and format it. +//! After dispatch, `emit_ocsf_event` clears the thread-local. use crate::events::OcsfEvent; std::thread_local! { // Thread-local storage for the current OCSF event being emitted. - // Used by the tracing layers to retrieve the full OcsfEvent struct. + // Layers clone from this; only emit_ocsf_event clears it. static CURRENT_EVENT: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; } /// Target string used to identify OCSF tracing events. -pub static OCSF_TARGET: &str = "ocsf"; - -/// Sentinel field name on the tracing event that signals an OCSF event is available. -static _OCSF_FIELD: OnceLock<&str> = OnceLock::new(); +pub const OCSF_TARGET: &str = "ocsf"; -/// Retrieve (and take) the current thread-local OCSF event, if any. -pub fn take_current_event() -> Option { - CURRENT_EVENT.with(|cell| cell.borrow_mut().take()) +/// Clone the current thread-local OCSF event, if any. +/// +/// Multiple layers can call this for the same event — each receives +/// an independent clone. The thread-local is only cleared by +/// `emit_ocsf_event` after tracing dispatch completes. +pub fn clone_current_event() -> Option { + CURRENT_EVENT.with(|cell| cell.borrow().clone()) } /// Emit an `OcsfEvent` through the tracing subscriber. /// /// The OCSF layers (`OcsfShorthandLayer`, `OcsfJsonlLayer`) format it /// as shorthand (`openshell.log`) and JSONL (`openshell-ocsf.log`). +/// +/// Both layers receive the event — `clone_current_event()` is non-consuming. pub fn emit_ocsf_event(event: OcsfEvent) { // Store the event in thread-local so layers can access it CURRENT_EVENT.with(|cell| { @@ -39,10 +41,10 @@ pub fn emit_ocsf_event(event: OcsfEvent) { }); // Emit a tracing event with the `ocsf` target. - // The layers detect this target and pull the OcsfEvent from thread-local. + // The layers detect this target and clone the OcsfEvent from thread-local. tracing::info!(target: "ocsf", "ocsf_event"); - // Clean up if layers didn't consume it (e.g., no OCSF layers registered) + // Clear the thread-local after dispatch completes. CURRENT_EVENT.with(|cell| { cell.borrow_mut().take(); }); @@ -69,9 +71,8 @@ mod tests { use crate::events::{BaseEvent, OcsfEvent}; use crate::objects::{Metadata, Product}; - #[test] - fn test_thread_local_store_and_take() { - let event = OcsfEvent::Base(BaseEvent { + fn test_event() -> OcsfEvent { + OcsfEvent::Base(BaseEvent { base: BaseEventData::new( 0, "Base Event", @@ -88,17 +89,44 @@ mod tests { log_source: None, }, ), + }) + } + + #[test] + fn test_clone_current_event_is_non_consuming() { + CURRENT_EVENT.with(|cell| { + *cell.borrow_mut() = Some(test_event()); }); + // First clone succeeds + let first = clone_current_event(); + assert!(first.is_some()); + assert_eq!(first.unwrap().class_uid(), 0); + + // Second clone also succeeds — non-consuming + let second = clone_current_event(); + assert!(second.is_some()); + assert_eq!(second.unwrap().class_uid(), 0); + + // Clean up CURRENT_EVENT.with(|cell| { - *cell.borrow_mut() = Some(event); + cell.borrow_mut().take(); }); + } - let taken = take_current_event(); - assert!(taken.is_some()); - assert_eq!(taken.unwrap().class_uid(), 0); + #[test] + fn test_emit_clears_thread_local_after_dispatch() { + // Manually store an event + CURRENT_EVENT.with(|cell| { + *cell.borrow_mut() = Some(test_event()); + }); + + // Clear it the same way emit_ocsf_event does after dispatch + CURRENT_EVENT.with(|cell| { + cell.borrow_mut().take(); + }); - // Second take should be None - assert!(take_current_event().is_none()); + // Should be empty now + assert!(clone_current_event().is_none()); } } diff --git a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs index c96c0ced..d9cf467d 100644 --- a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs @@ -7,10 +7,10 @@ use std::io::Write; use std::sync::Mutex; use tracing::Subscriber; -use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; +use tracing_subscriber::Layer; -use super::event_bridge::{OCSF_TARGET, take_current_event}; +use super::event_bridge::{clone_current_event, OCSF_TARGET}; /// A tracing `Layer` that intercepts OCSF events and writes JSONL output. /// @@ -39,7 +39,7 @@ where return; } - if let Some(ocsf_event) = take_current_event() { + if let Some(ocsf_event) = clone_current_event() { let line = ocsf_event.to_json_line(); if let Ok(mut w) = self.writer.lock() { let _ = w.write_all(line.as_bytes()); diff --git a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs index 488480fd..dc6b8fe1 100644 --- a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs @@ -7,10 +7,10 @@ use std::io::Write; use std::sync::Mutex; use tracing::Subscriber; -use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; +use tracing_subscriber::Layer; -use super::event_bridge::{OCSF_TARGET, take_current_event}; +use super::event_bridge::{clone_current_event, OCSF_TARGET}; /// A tracing `Layer` that intercepts OCSF events and writes shorthand output. /// @@ -49,8 +49,8 @@ where let meta = event.metadata(); if meta.target() == OCSF_TARGET { - // This is an OCSF event — pull from thread-local - if let Some(ocsf_event) = take_current_event() { + // This is an OCSF event — clone from thread-local (non-consuming) + if let Some(ocsf_event) = clone_current_event() { let line = ocsf_event.format_shorthand(); if let Ok(mut w) = self.writer.lock() { let _ = writeln!(w, "{line}"); From d508e4d1cf284920a6fdd3b2cd9e14c9ec9c9969 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:15:30 -0700 Subject: [PATCH 05/11] fix(ocsf): handle out-of-range timestamps without panicking Replace .unwrap() in format_ts with pattern match that returns a placeholder string for invalid timestamps. Prevents a panic in the sandbox supervisor if a corrupt timestamp reaches the formatter. --- crates/openshell-ocsf/src/format/shorthand.rs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/openshell-ocsf/src/format/shorthand.rs b/crates/openshell-ocsf/src/format/shorthand.rs index 143a126f..28487d12 100644 --- a/crates/openshell-ocsf/src/format/shorthand.rs +++ b/crates/openshell-ocsf/src/format/shorthand.rs @@ -9,11 +9,16 @@ use crate::events::OcsfEvent; use crate::objects::Url; /// Format a timestamp (ms since epoch) as `HH:MM:SS.mmm`. +/// +/// Returns a placeholder `"??:??:??.???"` for out-of-range timestamps +/// instead of panicking. #[must_use] pub fn format_ts(time_ms: i64) -> String { - use chrono::{TimeZone, Utc}; - let dt = Utc.timestamp_millis_opt(time_ms).unwrap(); - dt.format("%H:%M:%S%.3f").to_string() + use chrono::{MappedLocalTime, TimeZone, Utc}; + match Utc.timestamp_millis_opt(time_ms) { + MappedLocalTime::Single(dt) => dt.format("%H:%M:%S%.3f").to_string(), + _ => "??:??:??.???".to_string(), + } } /// Map a severity ID byte to its single-character shorthand. @@ -271,6 +276,16 @@ mod tests { assert_eq!(ts, "14:00:00.000"); } + #[test] + fn test_format_ts_invalid_timestamp_does_not_panic() { + // Out-of-range timestamp should return placeholder, not panic + let ts = format_ts(i64::MAX); + assert_eq!(ts, "??:??:??.???"); + + let ts = format_ts(i64::MIN); + assert_eq!(ts, "??:??:??.???"); + } + #[test] fn test_severity_char_mapping() { assert_eq!(severity_char(0), ' '); From edd20c48a2720b2857b3db79a7bc77f90e7ea7e4 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:16:29 -0700 Subject: [PATCH 06/11] refactor(ocsf): return Result from to_json and to_json_line Replace expect() calls with proper Result returns so callers can handle serialization errors instead of panicking. Updates the JSONL layer and all builder tests to propagate or unwrap the Result. --- crates/openshell-ocsf/src/builders/base.rs | 2 +- crates/openshell-ocsf/src/builders/config.rs | 2 +- crates/openshell-ocsf/src/builders/finding.rs | 2 +- crates/openshell-ocsf/src/builders/http.rs | 2 +- .../openshell-ocsf/src/builders/lifecycle.rs | 2 +- crates/openshell-ocsf/src/builders/network.rs | 2 +- crates/openshell-ocsf/src/builders/process.rs | 4 ++-- crates/openshell-ocsf/src/builders/ssh.rs | 2 +- crates/openshell-ocsf/src/format/jsonl.rs | 23 ++++++++----------- .../src/tracing_layers/jsonl_layer.rs | 7 +++--- 10 files changed, 23 insertions(+), 25 deletions(-) diff --git a/crates/openshell-ocsf/src/builders/base.rs b/crates/openshell-ocsf/src/builders/base.rs index 5141bff9..791e5a09 100644 --- a/crates/openshell-ocsf/src/builders/base.rs +++ b/crates/openshell-ocsf/src/builders/base.rs @@ -105,7 +105,7 @@ mod tests { .unmapped("host_ip", serde_json::json!("10.42.0.1")) .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 0); assert_eq!(json["activity_name"], "Network Namespace Created"); assert_eq!(json["message"], "Network namespace created"); diff --git a/crates/openshell-ocsf/src/builders/config.rs b/crates/openshell-ocsf/src/builders/config.rs index 05d62626..5f14a6b2 100644 --- a/crates/openshell-ocsf/src/builders/config.rs +++ b/crates/openshell-ocsf/src/builders/config.rs @@ -135,7 +135,7 @@ mod tests { .message("Policy reloaded successfully") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 5019); assert_eq!(json["state_id"], 2); assert_eq!(json["security_level"], "Secure"); diff --git a/crates/openshell-ocsf/src/builders/finding.rs b/crates/openshell-ocsf/src/builders/finding.rs index 1d38f822..f3246d89 100644 --- a/crates/openshell-ocsf/src/builders/finding.rs +++ b/crates/openshell-ocsf/src/builders/finding.rs @@ -212,7 +212,7 @@ mod tests { .message("NSSH1 nonce replay detected") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 2004); assert_eq!(json["finding_info"]["title"], "NSSH1 Nonce Replay Attack"); assert_eq!(json["is_alert"], true); diff --git a/crates/openshell-ocsf/src/builders/http.rs b/crates/openshell-ocsf/src/builders/http.rs index 449c2f66..ddd0a270 100644 --- a/crates/openshell-ocsf/src/builders/http.rs +++ b/crates/openshell-ocsf/src/builders/http.rs @@ -171,7 +171,7 @@ mod tests { .firewall_rule("default-egress", "mechanistic") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 4002); assert_eq!(json["activity_name"], "Get"); assert_eq!(json["http_request"]["http_method"], "GET"); diff --git a/crates/openshell-ocsf/src/builders/lifecycle.rs b/crates/openshell-ocsf/src/builders/lifecycle.rs index b1f42d19..b0d3a600 100644 --- a/crates/openshell-ocsf/src/builders/lifecycle.rs +++ b/crates/openshell-ocsf/src/builders/lifecycle.rs @@ -95,7 +95,7 @@ mod tests { .message("Starting sandbox") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 6002); assert_eq!(json["activity_name"], "Start"); assert_eq!(json["app"]["name"], "OpenShell Sandbox Supervisor"); diff --git a/crates/openshell-ocsf/src/builders/network.rs b/crates/openshell-ocsf/src/builders/network.rs index f0f84e50..e8da860b 100644 --- a/crates/openshell-ocsf/src/builders/network.rs +++ b/crates/openshell-ocsf/src/builders/network.rs @@ -220,7 +220,7 @@ mod tests { .message("CONNECT api.example.com:443 allowed") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 4001); assert_eq!(json["activity_name"], "Open"); assert_eq!(json["action"], "Allowed"); diff --git a/crates/openshell-ocsf/src/builders/process.rs b/crates/openshell-ocsf/src/builders/process.rs index 016816bf..efbeafa1 100644 --- a/crates/openshell-ocsf/src/builders/process.rs +++ b/crates/openshell-ocsf/src/builders/process.rs @@ -150,7 +150,7 @@ mod tests { .message("Process started: python3 /app/main.py") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 1007); assert_eq!(json["launch_type"], "Spawn"); assert_eq!(json["process"]["name"], "python3"); @@ -167,7 +167,7 @@ mod tests { .exit_code(0) .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["exit_code"], 0); } } diff --git a/crates/openshell-ocsf/src/builders/ssh.rs b/crates/openshell-ocsf/src/builders/ssh.rs index efa28d40..cc5b3ff3 100644 --- a/crates/openshell-ocsf/src/builders/ssh.rs +++ b/crates/openshell-ocsf/src/builders/ssh.rs @@ -166,7 +166,7 @@ mod tests { .message("SSH handshake accepted via NSSH1") .build(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 4007); assert_eq!(json["auth_type"], "NSSH1"); assert_eq!(json["auth_type_id"], 99); diff --git a/crates/openshell-ocsf/src/format/jsonl.rs b/crates/openshell-ocsf/src/format/jsonl.rs index 59d16cc2..8f3606ba 100644 --- a/crates/openshell-ocsf/src/format/jsonl.rs +++ b/crates/openshell-ocsf/src/format/jsonl.rs @@ -8,19 +8,16 @@ use crate::events::OcsfEvent; impl OcsfEvent { /// Serialize to a `serde_json::Value`. /// - /// Returns the full OCSF JSON object. - #[must_use] - pub fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).expect("OcsfEvent serialization should never fail") + /// Returns the full OCSF JSON object, or an error if serialization fails. + pub fn to_json(&self) -> Result { + serde_json::to_value(self) } /// Serialize as a single JSONL line (no pretty-printing, trailing newline). - #[must_use] - pub fn to_json_line(&self) -> String { - let mut line = - serde_json::to_string(self).expect("OcsfEvent serialization should never fail"); + pub fn to_json_line(&self) -> Result { + let mut line = serde_json::to_string(self)?; line.push('\n'); - line + Ok(line) } } @@ -56,7 +53,7 @@ mod tests { #[test] fn test_to_json_has_required_fields() { let event = test_event(); - let json = event.to_json(); + let json = event.to_json().unwrap(); assert_eq!(json["class_uid"], 0); assert_eq!(json["class_name"], "Base Event"); @@ -72,7 +69,7 @@ mod tests { #[test] fn test_to_json_line_format() { let event = test_event(); - let line = event.to_json_line(); + let line = event.to_json_line().unwrap(); // Must be a single line ending with \n assert!(line.ends_with('\n')); @@ -80,13 +77,13 @@ mod tests { // Must parse back to the same JSON let parsed: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); - assert_eq!(parsed, event.to_json()); + assert_eq!(parsed, event.to_json().unwrap()); } #[test] fn test_optional_fields_omitted() { let event = test_event(); - let json = event.to_json(); + let json = event.to_json().unwrap(); // Optional fields should not appear when None assert!(json.get("device").is_none()); diff --git a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs index d9cf467d..a63a938f 100644 --- a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs @@ -40,9 +40,10 @@ where } if let Some(ocsf_event) = clone_current_event() { - let line = ocsf_event.to_json_line(); - if let Ok(mut w) = self.writer.lock() { - let _ = w.write_all(line.as_bytes()); + if let Ok(line) = ocsf_event.to_json_line() { + if let Ok(mut w) = self.writer.lock() { + let _ = w.write_all(line.as_bytes()); + } } } } From 39324b42691a2045644bf1c39d4b60208671c0f8 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:17:30 -0700 Subject: [PATCH 07/11] refactor(ocsf): use crate:: imports instead of super:: in tracing layers Make event_bridge module pub(crate) and update both layer files to use crate::tracing_layers::event_bridge:: paths instead of super::. --- crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs | 2 +- crates/openshell-ocsf/src/tracing_layers/mod.rs | 2 +- crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs index a63a938f..5fc4c93e 100644 --- a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs @@ -10,7 +10,7 @@ use tracing::Subscriber; use tracing_subscriber::layer::Context; use tracing_subscriber::Layer; -use super::event_bridge::{clone_current_event, OCSF_TARGET}; +use crate::tracing_layers::event_bridge::{clone_current_event, OCSF_TARGET}; /// A tracing `Layer` that intercepts OCSF events and writes JSONL output. /// diff --git a/crates/openshell-ocsf/src/tracing_layers/mod.rs b/crates/openshell-ocsf/src/tracing_layers/mod.rs index 738437f0..a8213a29 100644 --- a/crates/openshell-ocsf/src/tracing_layers/mod.rs +++ b/crates/openshell-ocsf/src/tracing_layers/mod.rs @@ -7,7 +7,7 @@ //! - [`OcsfJsonlLayer`] writes OCSF JSONL to a writer //! - [`emit_ocsf_event`] bridges `OcsfEvent` structs into the tracing system -mod event_bridge; +pub(crate) mod event_bridge; mod jsonl_layer; mod shorthand_layer; diff --git a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs index dc6b8fe1..ddd1c1a7 100644 --- a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs @@ -10,7 +10,7 @@ use tracing::Subscriber; use tracing_subscriber::layer::Context; use tracing_subscriber::Layer; -use super::event_bridge::{clone_current_event, OCSF_TARGET}; +use crate::tracing_layers::event_bridge::{clone_current_event, OCSF_TARGET}; /// A tracing `Layer` that intercepts OCSF events and writes shorthand output. /// From 7cc8b9750685b26a5d8f10acfa0bbb3d82b08d2b Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:18:14 -0700 Subject: [PATCH 08/11] refactor(ocsf): collapse nested if-let chains per clippy guidance Flatten the nested if-let blocks in the JSONL layer's on_event handler into a single chained let expression. --- .../openshell-ocsf/src/tracing_layers/jsonl_layer.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs index 5fc4c93e..1d83f2e1 100644 --- a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs @@ -39,12 +39,11 @@ where return; } - if let Some(ocsf_event) = clone_current_event() { - if let Ok(line) = ocsf_event.to_json_line() { - if let Ok(mut w) = self.writer.lock() { - let _ = w.write_all(line.as_bytes()); - } - } + if let Some(ocsf_event) = clone_current_event() + && let Ok(line) = ocsf_event.to_json_line() + && let Ok(mut w) = self.writer.lock() + { + let _ = w.write_all(line.as_bytes()); } } } From 2b883f08ff945c3af6100322f12951740d841bb4 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 08:19:09 -0700 Subject: [PATCH 09/11] chore: mark vendored OCSF schemas as linguist-generated Add .gitattributes rule so GitHub excludes the vendored OCSF JSON schemas from diffs, language stats, and code search. --- .gitattributes | 3 +++ crates/openshell-ocsf/src/events/mod.rs | 6 +++--- crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs | 4 ++-- crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 78b56857..af03ae26 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,6 @@ python/openshell/_proto/*_pb2.pyi linguist-generated # Generated Rust protobuf code (excludes hand-written mod.rs) crates/openshell-core/src/proto/openshell.*.rs linguist-generated + +# Vendored OCSF schemas fetched from schema.ocsf.io +crates/openshell-ocsf/schemas/** linguist-generated diff --git a/crates/openshell-ocsf/src/events/mod.rs b/crates/openshell-ocsf/src/events/mod.rs index bd7526d7..d3584e79 100644 --- a/crates/openshell-ocsf/src/events/mod.rs +++ b/crates/openshell-ocsf/src/events/mod.rs @@ -165,9 +165,9 @@ impl OcsfEvent { mod tests { use super::*; use crate::builders::{ - test_sandbox_context, AppLifecycleBuilder, BaseEventBuilder, ConfigStateChangeBuilder, - DetectionFindingBuilder, HttpActivityBuilder, NetworkActivityBuilder, - ProcessActivityBuilder, SshActivityBuilder, + AppLifecycleBuilder, BaseEventBuilder, ConfigStateChangeBuilder, DetectionFindingBuilder, + HttpActivityBuilder, NetworkActivityBuilder, ProcessActivityBuilder, SshActivityBuilder, + test_sandbox_context, }; use crate::enums::*; use crate::objects::*; diff --git a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs index 1d83f2e1..4466c0ab 100644 --- a/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/jsonl_layer.rs @@ -7,10 +7,10 @@ use std::io::Write; use std::sync::Mutex; use tracing::Subscriber; -use tracing_subscriber::layer::Context; use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; -use crate::tracing_layers::event_bridge::{clone_current_event, OCSF_TARGET}; +use crate::tracing_layers::event_bridge::{OCSF_TARGET, clone_current_event}; /// A tracing `Layer` that intercepts OCSF events and writes JSONL output. /// diff --git a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs index ddd1c1a7..f8a39f6a 100644 --- a/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs +++ b/crates/openshell-ocsf/src/tracing_layers/shorthand_layer.rs @@ -7,10 +7,10 @@ use std::io::Write; use std::sync::Mutex; use tracing::Subscriber; -use tracing_subscriber::layer::Context; use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; -use crate::tracing_layers::event_bridge::{clone_current_event, OCSF_TARGET}; +use crate::tracing_layers::event_bridge::{OCSF_TARGET, clone_current_event}; /// A tracing `Layer` that intercepts OCSF events and writes shorthand output. /// From 5e134ca20658278a3450247aa61c03566ff50131 Mon Sep 17 00:00:00 2001 From: John Myers Date: Fri, 20 Mar 2026 09:09:53 -0700 Subject: [PATCH 10/11] refactor(ocsf): replace redundant id+label pairs with typed enums Store typed enum values (ActionId, DispositionId, SeverityId, etc.) directly in event structs instead of separate _id: u8 + label: String pairs. Labels are derived at serialization time via custom Serialize impls, eliminating the drift risk between ID and label fields. - Add OcsfEnum trait implemented by all 11 enum types - Add HttpMethod enum (9 OCSF-defined variants + Other) for HttpRequest - Refactor BaseEventData: severity and status use typed enums - Refactor all 6 event structs: 18 id+label pairs consolidated to single typed fields with derive(Deserialize) + custom Serialize - 2 custom-label fields (auth_type, state) use separate _custom_label field for the Other variant override - Simplify OcsfEvent Serialize to delegate directly to inner structs - Simplify all 8 builders: remove manual .as_u8()/.label() expansion - Add serde_helpers module with insert_enum_pair! macros --- crates/openshell-ocsf/src/builders/config.rs | 10 +- crates/openshell-ocsf/src/builders/finding.rs | 12 +- crates/openshell-ocsf/src/builders/http.rs | 6 +- crates/openshell-ocsf/src/builders/network.rs | 6 +- crates/openshell-ocsf/src/builders/process.rs | 9 +- crates/openshell-ocsf/src/builders/ssh.rs | 10 +- .../openshell-ocsf/src/enums/http_method.rs | 137 ++++++++++++++++++ crates/openshell-ocsf/src/enums/mod.rs | 39 +++++ .../openshell-ocsf/src/events/base_event.rs | 78 +++++++--- .../src/events/config_state_change.rs | 63 ++++---- .../src/events/detection_finding.rs | 92 +++++++----- .../src/events/http_activity.rs | 64 +++++--- crates/openshell-ocsf/src/events/mod.rs | 45 ++---- .../src/events/network_activity.rs | 61 +++++--- .../src/events/process_activity.rs | 68 +++++---- .../src/events/serde_helpers.rs | 71 +++++++++ .../openshell-ocsf/src/events/ssh_activity.rs | 81 +++++++---- crates/openshell-ocsf/src/format/shorthand.rs | 93 ++++++------ crates/openshell-ocsf/src/lib.rs | 4 +- crates/openshell-ocsf/src/objects/http.rs | 6 +- 20 files changed, 657 insertions(+), 298 deletions(-) create mode 100644 crates/openshell-ocsf/src/enums/http_method.rs create mode 100644 crates/openshell-ocsf/src/events/serde_helpers.rs diff --git a/crates/openshell-ocsf/src/builders/config.rs b/crates/openshell-ocsf/src/builders/config.rs index 5f14a6b2..8ff9cae2 100644 --- a/crates/openshell-ocsf/src/builders/config.rs +++ b/crates/openshell-ocsf/src/builders/config.rs @@ -106,12 +106,10 @@ impl<'a> ConfigStateChangeBuilder<'a> { OcsfEvent::DeviceConfigStateChange(DeviceConfigStateChangeEvent { base, - state_id: self.state_id.map(StateId::as_u8), - state: self.state_label, - security_level_id: self.security_level.map(SecurityLevelId::as_u8), - security_level: self.security_level.map(|s| s.label().to_string()), - prev_security_level_id: self.prev_security_level.map(SecurityLevelId::as_u8), - prev_security_level: self.prev_security_level.map(|s| s.label().to_string()), + state: self.state_id, + state_custom_label: self.state_label, + security_level: self.security_level, + prev_security_level: self.prev_security_level, }) } } diff --git a/crates/openshell-ocsf/src/builders/finding.rs b/crates/openshell-ocsf/src/builders/finding.rs index f3246d89..10d770f4 100644 --- a/crates/openshell-ocsf/src/builders/finding.rs +++ b/crates/openshell-ocsf/src/builders/finding.rs @@ -170,14 +170,10 @@ impl<'a> DetectionFindingBuilder<'a> { }, remediation: self.remediation, is_alert: self.is_alert, - confidence_id: self.confidence.map(ConfidenceId::as_u8), - confidence: self.confidence.map(|c| c.label().to_string()), - risk_level_id: self.risk_level.map(RiskLevelId::as_u8), - risk_level: self.risk_level.map(|r| r.label().to_string()), - action_id: self.action.map(ActionId::as_u8), - action: self.action.map(|a| a.label().to_string()), - disposition_id: self.disposition.map(DispositionId::as_u8), - disposition: self.disposition.map(|d| d.label().to_string()), + confidence: self.confidence, + risk_level: self.risk_level, + action: self.action, + disposition: self.disposition, }) } } diff --git a/crates/openshell-ocsf/src/builders/http.rs b/crates/openshell-ocsf/src/builders/http.rs index ddd0a270..9cc49c9e 100644 --- a/crates/openshell-ocsf/src/builders/http.rs +++ b/crates/openshell-ocsf/src/builders/http.rs @@ -139,10 +139,8 @@ impl<'a> HttpActivityBuilder<'a> { proxy_endpoint: Some(self.ctx.proxy_endpoint()), actor: self.actor, firewall_rule: self.firewall_rule, - action_id: self.action.map(ActionId::as_u8), - action: self.action.map(|a| a.label().to_string()), - disposition_id: self.disposition.map(DispositionId::as_u8), - disposition: self.disposition.map(|d| d.label().to_string()), + action: self.action, + disposition: self.disposition, observation_point_id: Some(2), is_src_dst_assignment_known: Some(true), }) diff --git a/crates/openshell-ocsf/src/builders/network.rs b/crates/openshell-ocsf/src/builders/network.rs index e8da860b..d0a79925 100644 --- a/crates/openshell-ocsf/src/builders/network.rs +++ b/crates/openshell-ocsf/src/builders/network.rs @@ -189,10 +189,8 @@ impl<'a> NetworkActivityBuilder<'a> { actor: self.actor, firewall_rule: self.firewall_rule, connection_info: self.connection_info, - action_id: self.action.map(ActionId::as_u8), - action: self.action.map(|a| a.label().to_string()), - disposition_id: self.disposition.map(DispositionId::as_u8), - disposition: self.disposition.map(|d| d.label().to_string()), + action: self.action, + disposition: self.disposition, observation_point_id: self.observation_point_id, is_src_dst_assignment_known: Some(true), }) diff --git a/crates/openshell-ocsf/src/builders/process.rs b/crates/openshell-ocsf/src/builders/process.rs index efbeafa1..8ede8012 100644 --- a/crates/openshell-ocsf/src/builders/process.rs +++ b/crates/openshell-ocsf/src/builders/process.rs @@ -120,13 +120,10 @@ impl<'a> ProcessActivityBuilder<'a> { base, process: self.process.unwrap_or_else(|| Process::new("unknown", 0)), actor: self.actor, - launch_type_id: self.launch_type.map(LaunchTypeId::as_u8), - launch_type: self.launch_type.map(|lt| lt.label().to_string()), + launch_type: self.launch_type, exit_code: self.exit_code, - action_id: self.action.map(ActionId::as_u8), - action: self.action.map(|a| a.label().to_string()), - disposition_id: self.disposition.map(DispositionId::as_u8), - disposition: self.disposition.map(|d| d.label().to_string()), + action: self.action, + disposition: self.disposition, }) } } diff --git a/crates/openshell-ocsf/src/builders/ssh.rs b/crates/openshell-ocsf/src/builders/ssh.rs index cc5b3ff3..6df01f3d 100644 --- a/crates/openshell-ocsf/src/builders/ssh.rs +++ b/crates/openshell-ocsf/src/builders/ssh.rs @@ -136,13 +136,11 @@ impl<'a> SshActivityBuilder<'a> { src_endpoint: self.src_endpoint, dst_endpoint: self.dst_endpoint, actor: self.actor, - auth_type_id: self.auth_type_id.map(AuthTypeId::as_u8), - auth_type: self.auth_type_label, + auth_type: self.auth_type_id, + auth_type_custom_label: self.auth_type_label, protocol_ver: self.protocol_ver, - action_id: self.action.map(ActionId::as_u8), - action: self.action.map(|a| a.label().to_string()), - disposition_id: self.disposition.map(DispositionId::as_u8), - disposition: self.disposition.map(|d| d.label().to_string()), + action: self.action, + disposition: self.disposition, }) } } diff --git a/crates/openshell-ocsf/src/enums/http_method.rs b/crates/openshell-ocsf/src/enums/http_method.rs new file mode 100644 index 00000000..b7b04e09 --- /dev/null +++ b/crates/openshell-ocsf/src/enums/http_method.rs @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF `http_method` enum — the 9 OCSF-defined HTTP methods. + +use serde::{Deserialize, Serialize}; + +/// HTTP method as defined in the OCSF v1.7.0 `http_request` object schema. +/// +/// The 9 standard methods are typed variants. Non-standard methods use +/// `Other(String)`. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum HttpMethod { + /// OPTIONS + Options, + /// GET + Get, + /// HEAD + Head, + /// POST + Post, + /// PUT + Put, + /// DELETE + Delete, + /// TRACE + Trace, + /// CONNECT + Connect, + /// PATCH + Patch, + /// Non-standard method. + Other(String), +} + +impl HttpMethod { + /// Return the canonical uppercase string representation. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Options => "OPTIONS", + Self::Get => "GET", + Self::Head => "HEAD", + Self::Post => "POST", + Self::Put => "PUT", + Self::Delete => "DELETE", + Self::Trace => "TRACE", + Self::Connect => "CONNECT", + Self::Patch => "PATCH", + Self::Other(s) => s, + } + } +} + +impl std::str::FromStr for HttpMethod { + type Err = std::convert::Infallible; + + /// Parse a method string into a typed variant (case-insensitive). + fn from_str(s: &str) -> Result { + Ok(match s.to_uppercase().as_str() { + "OPTIONS" => Self::Options, + "GET" => Self::Get, + "HEAD" => Self::Head, + "POST" => Self::Post, + "PUT" => Self::Put, + "DELETE" => Self::Delete, + "TRACE" => Self::Trace, + "CONNECT" => Self::Connect, + "PATCH" => Self::Patch, + _ => Self::Other(s.to_string()), + }) + } +} + +impl Serialize for HttpMethod { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for HttpMethod { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Ok(s.parse().unwrap()) + } +} + +impl std::fmt::Display for HttpMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_str_standard_methods() { + assert_eq!("GET".parse::().unwrap(), HttpMethod::Get); + assert_eq!("get".parse::().unwrap(), HttpMethod::Get); + assert_eq!("Post".parse::().unwrap(), HttpMethod::Post); + assert_eq!("DELETE".parse::().unwrap(), HttpMethod::Delete); + assert_eq!( + "CONNECT".parse::().unwrap(), + HttpMethod::Connect + ); + assert_eq!("PATCH".parse::().unwrap(), HttpMethod::Patch); + } + + #[test] + fn test_from_str_non_standard() { + let method: HttpMethod = "PROPFIND".parse().unwrap(); + assert_eq!(method, HttpMethod::Other("PROPFIND".to_string())); + assert_eq!(method.as_str(), "PROPFIND"); + } + + #[test] + fn test_json_roundtrip() { + let method = HttpMethod::Get; + let json = serde_json::to_value(&method).unwrap(); + assert_eq!(json, serde_json::json!("GET")); + + let deserialized: HttpMethod = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, HttpMethod::Get); + } + + #[test] + fn test_json_roundtrip_other() { + let method = HttpMethod::Other("PROPFIND".to_string()); + let json = serde_json::to_value(&method).unwrap(); + assert_eq!(json, serde_json::json!("PROPFIND")); + + let deserialized: HttpMethod = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, HttpMethod::Other("PROPFIND".to_string())); + } +} diff --git a/crates/openshell-ocsf/src/enums/mod.rs b/crates/openshell-ocsf/src/enums/mod.rs index 2bddd77e..8ab03eb6 100644 --- a/crates/openshell-ocsf/src/enums/mod.rs +++ b/crates/openshell-ocsf/src/enums/mod.rs @@ -7,6 +7,7 @@ mod action; mod activity; mod auth; mod disposition; +mod http_method; mod launch; mod security; mod severity; @@ -16,7 +17,45 @@ pub use action::ActionId; pub use activity::ActivityId; pub use auth::AuthTypeId; pub use disposition::DispositionId; +pub use http_method::HttpMethod; pub use launch::LaunchTypeId; pub use security::{ConfidenceId, RiskLevelId, SecurityLevelId}; pub use severity::SeverityId; pub use status::{StateId, StatusId}; + +/// Trait for OCSF enum types that have an integer ID and a string label. +/// +/// All OCSF "sibling pair" enums implement this trait, enabling generic +/// serialization of `_id` + label field pairs. +pub trait OcsfEnum: Copy + Clone + PartialEq + Eq + std::fmt::Debug { + /// Return the integer representation for JSON serialization. + fn as_u8(self) -> u8; + + /// Return the OCSF string label for this value. + fn label(self) -> &'static str; +} + +/// Implement [`OcsfEnum`] for a type that already has `as_u8()` and `label()` methods. +macro_rules! impl_ocsf_enum { + ($($ty:ty),+ $(,)?) => { + $( + impl OcsfEnum for $ty { + fn as_u8(self) -> u8 { self.as_u8() } + fn label(self) -> &'static str { self.label() } + } + )+ + }; +} + +impl_ocsf_enum!( + ActionId, + AuthTypeId, + ConfidenceId, + DispositionId, + LaunchTypeId, + RiskLevelId, + SecurityLevelId, + SeverityId, + StateId, + StatusId, +); diff --git a/crates/openshell-ocsf/src/events/base_event.rs b/crates/openshell-ocsf/src/events/base_event.rs index f8568fe6..97e7c038 100644 --- a/crates/openshell-ocsf/src/events/base_event.rs +++ b/crates/openshell-ocsf/src/events/base_event.rs @@ -11,7 +11,7 @@ use crate::objects::{Container, Device, Metadata}; /// Common fields shared by all OCSF event classes. /// /// Every event class embeds this struct via `#[serde(flatten)]`. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct BaseEventData { /// OCSF class UID (e.g., 4001 for Network Activity). pub class_uid: u32, @@ -40,19 +40,13 @@ pub struct BaseEventData { /// Event timestamp in milliseconds since epoch. pub time: i64, - /// Severity ID. - pub severity_id: u8, - - /// Severity label. - pub severity: String, - - /// Status ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub status_id: Option, + /// Severity (typed enum, serialized as `severity_id` + `severity` pair). + #[serde(rename = "severity_id")] + pub severity: SeverityId, - /// Status label. - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, + /// Status (typed enum, serialized as `status_id` + `status` pair). + #[serde(rename = "status_id", default, skip_serializing_if = "Option::is_none")] + pub status: Option, /// Human-readable event message. #[serde(skip_serializing_if = "Option::is_none")] @@ -78,6 +72,54 @@ pub struct BaseEventData { pub unmapped: Option, } +impl Serialize for BaseEventData { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeMap; + + // Count fields: 9 required + severity pair (2) + up to 6 optional + let mut map = serializer.serialize_map(None)?; + + map.serialize_entry("class_uid", &self.class_uid)?; + map.serialize_entry("class_name", &self.class_name)?; + map.serialize_entry("category_uid", &self.category_uid)?; + map.serialize_entry("category_name", &self.category_name)?; + map.serialize_entry("activity_id", &self.activity_id)?; + map.serialize_entry("activity_name", &self.activity_name)?; + map.serialize_entry("type_uid", &self.type_uid)?; + map.serialize_entry("type_name", &self.type_name)?; + map.serialize_entry("time", &self.time)?; + + // Severity — typed enum → id + label pair + map.serialize_entry("severity_id", &self.severity.as_u8())?; + map.serialize_entry("severity", self.severity.label())?; + + // Status — optional typed enum → id + label pair + if let Some(status) = self.status { + map.serialize_entry("status_id", &status.as_u8())?; + map.serialize_entry("status", status.label())?; + } + + if let Some(ref msg) = self.message { + map.serialize_entry("message", msg)?; + } + if let Some(ref detail) = self.status_detail { + map.serialize_entry("status_detail", detail)?; + } + map.serialize_entry("metadata", &self.metadata)?; + 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 unmapped) = self.unmapped { + map.serialize_entry("unmapped", unmapped)?; + } + + map.end() + } +} + impl BaseEventData { /// Create base event data with required fields. #[allow(clippy::too_many_arguments)] @@ -105,9 +147,7 @@ impl BaseEventData { type_uid, type_name, time: chrono::Utc::now().timestamp_millis(), - severity_id: severity_id.as_u8(), - severity: severity_id.label().to_string(), - status_id: None, + severity: severity_id, status: None, message: None, status_detail: None, @@ -125,8 +165,7 @@ impl BaseEventData { /// Set status. pub fn set_status(&mut self, status_id: StatusId) { - self.status_id = Some(status_id.as_u8()); - self.status = Some(status_id.label().to_string()); + self.status = Some(status_id); } /// Set message. @@ -199,8 +238,7 @@ mod tests { assert_eq!(base.class_uid, 0); assert_eq!(base.type_uid, 99); // 0 * 100 + 99 assert_eq!(base.type_name, "Base Event: Other"); - assert_eq!(base.severity_id, 1); - assert_eq!(base.severity, "Informational"); + assert_eq!(base.severity, SeverityId::Informational); } #[test] diff --git a/crates/openshell-ocsf/src/events/config_state_change.rs b/crates/openshell-ocsf/src/events/config_state_change.rs index 8425be72..a28b4375 100644 --- a/crates/openshell-ocsf/src/events/config_state_change.rs +++ b/crates/openshell-ocsf/src/events/config_state_change.rs @@ -5,40 +5,55 @@ use serde::{Deserialize, Serialize}; +use crate::enums::{SecurityLevelId, StateId}; use crate::events::base_event::BaseEventData; /// OCSF Device Config State Change Event [5019]. /// /// Policy engine and inference routing configuration changes. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct DeviceConfigStateChangeEvent { /// Common base event fields. #[serde(flatten)] pub base: BaseEventData, - /// State ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub state_id: Option, + #[serde(rename = "state_id", default, skip_serializing_if = "Option::is_none")] + pub state: Option, - /// State label. - #[serde(skip_serializing_if = "Option::is_none")] - pub state: Option, + /// Custom state label (used when `state_id` maps to a non-standard label). + #[serde(rename = "state", default, skip_serializing_if = "Option::is_none")] + pub state_custom_label: Option, - /// Security level ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub security_level_id: Option, + #[serde( + rename = "security_level_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub security_level: Option, - /// Security level label. - #[serde(skip_serializing_if = "Option::is_none")] - pub security_level: Option, + #[serde( + rename = "prev_security_level_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub prev_security_level: Option, +} + +impl Serialize for DeviceConfigStateChangeEvent { + fn serialize(&self, serializer: S) -> Result { + use crate::events::serde_helpers::{insert_enum_pair, insert_enum_pair_custom}; - /// Previous security level ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub prev_security_level_id: Option, + let mut base_val = serde_json::to_value(&self.base).map_err(serde::ser::Error::custom)?; + let obj = base_val + .as_object_mut() + .ok_or_else(|| serde::ser::Error::custom("expected object"))?; - /// Previous security level label. - #[serde(skip_serializing_if = "Option::is_none")] - pub prev_security_level: Option, + insert_enum_pair_custom!(obj, "state", self.state, self.state_custom_label); + insert_enum_pair!(obj, "security_level", self.security_level); + insert_enum_pair!(obj, "prev_security_level", self.prev_security_level); + + base_val.serialize(serializer) + } } #[cfg(test)] @@ -71,12 +86,10 @@ mod tests { let event = DeviceConfigStateChangeEvent { base, - state_id: Some(StateId::Enabled.as_u8()), - state: Some("Enabled".to_string()), - security_level_id: Some(SecurityLevelId::Secure.as_u8()), - security_level: Some("Secure".to_string()), - prev_security_level_id: Some(SecurityLevelId::Unknown.as_u8()), - prev_security_level: Some("Unknown".to_string()), + state: Some(StateId::Enabled), + state_custom_label: None, + security_level: Some(SecurityLevelId::Secure), + prev_security_level: Some(SecurityLevelId::Unknown), }; let json = serde_json::to_value(&event).unwrap(); diff --git a/crates/openshell-ocsf/src/events/detection_finding.rs b/crates/openshell-ocsf/src/events/detection_finding.rs index b2a0e585..35ef222c 100644 --- a/crates/openshell-ocsf/src/events/detection_finding.rs +++ b/crates/openshell-ocsf/src/events/detection_finding.rs @@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize}; +use crate::enums::{ActionId, ConfidenceId, DispositionId, RiskLevelId}; use crate::events::base_event::BaseEventData; use crate::objects::{Attack, Evidence, FindingInfo, Remediation}; /// OCSF Detection Finding Event [2004]. /// /// Security-relevant findings from policy enforcement. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct DetectionFindingEvent { /// Common base event fields. #[serde(flatten)] @@ -36,43 +37,58 @@ pub struct DetectionFindingEvent { #[serde(skip_serializing_if = "Option::is_none")] pub is_alert: Option, - /// Confidence ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub confidence_id: Option, - - /// Confidence label. - #[serde(skip_serializing_if = "Option::is_none")] - pub confidence: Option, - - /// Risk level ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub risk_level_id: Option, - - /// Risk level label. - #[serde(skip_serializing_if = "Option::is_none")] - pub risk_level: Option, - - /// Action ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub action_id: Option, - - /// Action label. - #[serde(skip_serializing_if = "Option::is_none")] - pub action: Option, - - /// Disposition ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition_id: Option, + #[serde( + rename = "confidence_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub confidence: Option, + + #[serde( + rename = "risk_level_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub risk_level: Option, + + #[serde(rename = "action_id", default, skip_serializing_if = "Option::is_none")] + pub action: Option, + + #[serde( + rename = "disposition_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub disposition: Option, +} - /// Disposition label. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition: Option, +impl Serialize for DetectionFindingEvent { + fn serialize(&self, serializer: S) -> Result { + use crate::events::serde_helpers::{insert_enum_pair, insert_optional, insert_required}; + + let mut base_val = serde_json::to_value(&self.base).map_err(serde::ser::Error::custom)?; + let obj = base_val + .as_object_mut() + .ok_or_else(|| serde::ser::Error::custom("expected object"))?; + + insert_required!(obj, "finding_info", self.finding_info); + insert_optional!(obj, "evidences", self.evidences); + insert_optional!(obj, "attacks", self.attacks); + insert_optional!(obj, "remediation", self.remediation); + insert_optional!(obj, "is_alert", self.is_alert); + insert_enum_pair!(obj, "confidence", self.confidence); + insert_enum_pair!(obj, "risk_level", self.risk_level); + insert_enum_pair!(obj, "action", self.action); + insert_enum_pair!(obj, "disposition", self.disposition); + + base_val.serialize(serializer) + } } #[cfg(test)] mod tests { use super::*; - use crate::enums::SeverityId; + use crate::enums::{ActionId, ConfidenceId, DispositionId, RiskLevelId, SeverityId}; use crate::objects::{Metadata, Product}; #[test] @@ -108,14 +124,10 @@ mod tests { )]), remediation: None, is_alert: Some(true), - confidence_id: Some(3), - confidence: Some("High".to_string()), - risk_level_id: Some(4), - risk_level: Some("High".to_string()), - action_id: Some(2), - action: Some("Denied".to_string()), - disposition_id: Some(2), - disposition: Some("Blocked".to_string()), + confidence: Some(ConfidenceId::High), + risk_level: Some(RiskLevelId::High), + action: Some(ActionId::Denied), + disposition: Some(DispositionId::Blocked), }; let json = serde_json::to_value(&event).unwrap(); diff --git a/crates/openshell-ocsf/src/events/http_activity.rs b/crates/openshell-ocsf/src/events/http_activity.rs index 060f5f66..fe3b0357 100644 --- a/crates/openshell-ocsf/src/events/http_activity.rs +++ b/crates/openshell-ocsf/src/events/http_activity.rs @@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize}; +use crate::enums::{ActionId, DispositionId}; use crate::events::base_event::BaseEventData; use crate::objects::{Actor, Endpoint, FirewallRule, HttpRequest, HttpResponse}; /// OCSF HTTP Activity Event [4002]. /// /// HTTP-level events through the forward proxy and L7 relay. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct HttpActivityEvent { /// Common base event fields. #[serde(flatten)] @@ -45,21 +46,17 @@ pub struct HttpActivityEvent { #[serde(skip_serializing_if = "Option::is_none")] pub firewall_rule: Option, - /// Action ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub action_id: Option, + /// Action taken (typed enum serialized as `action_id` + `action` label). + #[serde(rename = "action_id", default, skip_serializing_if = "Option::is_none")] + pub action: Option, - /// Action label. - #[serde(skip_serializing_if = "Option::is_none")] - pub action: Option, - - /// Disposition ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition_id: Option, - - /// Disposition label. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition: Option, + /// Disposition (typed enum serialized as `disposition_id` + `disposition` label). + #[serde( + rename = "disposition_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub disposition: Option, /// Observation point ID (v1.6.0+). #[serde(skip_serializing_if = "Option::is_none")] @@ -70,10 +67,39 @@ pub struct HttpActivityEvent { pub is_src_dst_assignment_known: Option, } +impl Serialize for HttpActivityEvent { + fn serialize(&self, serializer: S) -> Result { + use crate::events::serde_helpers::{insert_enum_pair, insert_optional}; + + let mut base_val = serde_json::to_value(&self.base).map_err(serde::ser::Error::custom)?; + let obj = base_val + .as_object_mut() + .ok_or_else(|| serde::ser::Error::custom("expected object"))?; + + insert_optional!(obj, "http_request", self.http_request); + insert_optional!(obj, "http_response", self.http_response); + insert_optional!(obj, "src_endpoint", self.src_endpoint); + insert_optional!(obj, "dst_endpoint", self.dst_endpoint); + insert_optional!(obj, "proxy_endpoint", self.proxy_endpoint); + insert_optional!(obj, "actor", self.actor); + insert_optional!(obj, "firewall_rule", self.firewall_rule); + insert_enum_pair!(obj, "action", self.action); + insert_enum_pair!(obj, "disposition", self.disposition); + insert_optional!(obj, "observation_point_id", self.observation_point_id); + insert_optional!( + obj, + "is_src_dst_assignment_known", + self.is_src_dst_assignment_known + ); + + base_val.serialize(serializer) + } +} + #[cfg(test)] mod tests { use super::*; - use crate::enums::SeverityId; + use crate::enums::{ActionId, DispositionId, SeverityId}; use crate::objects::{Metadata, Product, Url}; #[test] @@ -105,10 +131,8 @@ mod tests { proxy_endpoint: None, actor: None, firewall_rule: None, - action_id: Some(1), - action: Some("Allowed".to_string()), - disposition_id: Some(1), - disposition: Some("Allowed".to_string()), + action: Some(ActionId::Allowed), + disposition: Some(DispositionId::Allowed), observation_point_id: None, is_src_dst_assignment_known: None, }; diff --git a/crates/openshell-ocsf/src/events/mod.rs b/crates/openshell-ocsf/src/events/mod.rs index d3584e79..23519e56 100644 --- a/crates/openshell-ocsf/src/events/mod.rs +++ b/crates/openshell-ocsf/src/events/mod.rs @@ -10,6 +10,7 @@ mod detection_finding; mod http_activity; mod network_activity; mod process_activity; +pub(crate) mod serde_helpers; mod ssh_activity; pub use app_lifecycle::ApplicationLifecycleEvent; @@ -21,12 +22,11 @@ pub use network_activity::NetworkActivityEvent; pub use process_activity::ProcessActivityEvent; pub use ssh_activity::SshActivityEvent; -use serde::ser::SerializeMap; use serde::{Deserialize, Serialize}; /// Top-level OCSF event enum encompassing all supported event classes. /// -/// Serialization uses the inner event struct directly (untagged). +/// Serialization delegates directly to the inner event struct (untagged). /// Deserialization dispatches on the `class_uid` field to select the /// correct variant, avoiding the ambiguity of `#[serde(untagged)]`. #[derive(Debug, Clone, PartialEq, Eq)] @@ -51,38 +51,15 @@ pub enum OcsfEvent { impl Serialize for OcsfEvent { fn serialize(&self, serializer: S) -> Result { - // Serialize the inner event struct directly — produces flat OCSF JSON. - // We serialize to a Value first to get the map, then re-serialize. - let value = match self { - Self::NetworkActivity(e) => { - serde_json::to_value(e).map_err(serde::ser::Error::custom)? - } - Self::HttpActivity(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, - Self::SshActivity(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, - Self::ProcessActivity(e) => { - serde_json::to_value(e).map_err(serde::ser::Error::custom)? - } - Self::DetectionFinding(e) => { - serde_json::to_value(e).map_err(serde::ser::Error::custom)? - } - Self::ApplicationLifecycle(e) => { - serde_json::to_value(e).map_err(serde::ser::Error::custom)? - } - Self::DeviceConfigStateChange(e) => { - serde_json::to_value(e).map_err(serde::ser::Error::custom)? - } - Self::Base(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, - }; - - // Re-serialize the flat JSON object - if let serde_json::Value::Object(map) = value { - let mut ser_map = serializer.serialize_map(Some(map.len()))?; - for (k, v) in &map { - ser_map.serialize_entry(k, v)?; - } - ser_map.end() - } else { - Err(serde::ser::Error::custom("expected JSON object")) + match self { + Self::NetworkActivity(e) => e.serialize(serializer), + Self::HttpActivity(e) => e.serialize(serializer), + Self::SshActivity(e) => e.serialize(serializer), + Self::ProcessActivity(e) => e.serialize(serializer), + Self::DetectionFinding(e) => e.serialize(serializer), + Self::ApplicationLifecycle(e) => e.serialize(serializer), + Self::DeviceConfigStateChange(e) => e.serialize(serializer), + Self::Base(e) => e.serialize(serializer), } } } diff --git a/crates/openshell-ocsf/src/events/network_activity.rs b/crates/openshell-ocsf/src/events/network_activity.rs index 0d611eb2..6cd125fd 100644 --- a/crates/openshell-ocsf/src/events/network_activity.rs +++ b/crates/openshell-ocsf/src/events/network_activity.rs @@ -5,13 +5,14 @@ use serde::{Deserialize, Serialize}; +use crate::enums::{ActionId, DispositionId}; use crate::events::base_event::BaseEventData; use crate::objects::{Actor, ConnectionInfo, Endpoint, FirewallRule}; /// OCSF Network Activity Event [4001]. /// /// Proxy CONNECT tunnel events and iptables-level bypass detection. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct NetworkActivityEvent { /// Common base event fields. #[serde(flatten)] @@ -41,21 +42,17 @@ pub struct NetworkActivityEvent { #[serde(skip_serializing_if = "Option::is_none")] pub connection_info: Option, - /// Action ID (Security Control profile). - #[serde(skip_serializing_if = "Option::is_none")] - pub action_id: Option, + /// Action (Security Control profile). + #[serde(rename = "action_id", default, skip_serializing_if = "Option::is_none")] + pub action: Option, - /// Action label. - #[serde(skip_serializing_if = "Option::is_none")] - pub action: Option, - - /// Disposition ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition_id: Option, - - /// Disposition label. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition: Option, + /// Disposition. + #[serde( + rename = "disposition_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub disposition: Option, /// Observation point ID (v1.6.0+). #[serde(skip_serializing_if = "Option::is_none")] @@ -66,6 +63,34 @@ pub struct NetworkActivityEvent { pub is_src_dst_assignment_known: Option, } +impl Serialize for NetworkActivityEvent { + fn serialize(&self, serializer: S) -> Result { + use crate::events::serde_helpers::{insert_enum_pair, insert_optional}; + + let mut base_val = serde_json::to_value(&self.base).map_err(serde::ser::Error::custom)?; + let obj = base_val + .as_object_mut() + .ok_or_else(|| serde::ser::Error::custom("expected object"))?; + + insert_optional!(obj, "src_endpoint", self.src_endpoint); + insert_optional!(obj, "dst_endpoint", self.dst_endpoint); + insert_optional!(obj, "proxy_endpoint", self.proxy_endpoint); + insert_optional!(obj, "actor", self.actor); + insert_optional!(obj, "firewall_rule", self.firewall_rule); + insert_optional!(obj, "connection_info", self.connection_info); + insert_enum_pair!(obj, "action", self.action); + insert_enum_pair!(obj, "disposition", self.disposition); + insert_optional!(obj, "observation_point_id", self.observation_point_id); + insert_optional!( + obj, + "is_src_dst_assignment_known", + self.is_src_dst_assignment_known + ); + + base_val.serialize(serializer) + } +} + #[cfg(test)] mod tests { use super::*; @@ -97,10 +122,8 @@ mod tests { actor: None, firewall_rule: Some(FirewallRule::new("default-egress", "mechanistic")), connection_info: None, - action_id: Some(ActionId::Allowed.as_u8()), - action: Some(ActionId::Allowed.label().to_string()), - disposition_id: Some(DispositionId::Allowed.as_u8()), - disposition: Some(DispositionId::Allowed.label().to_string()), + action: Some(ActionId::Allowed), + disposition: Some(DispositionId::Allowed), observation_point_id: Some(2), is_src_dst_assignment_known: Some(true), }; diff --git a/crates/openshell-ocsf/src/events/process_activity.rs b/crates/openshell-ocsf/src/events/process_activity.rs index 204dee73..0c5829f9 100644 --- a/crates/openshell-ocsf/src/events/process_activity.rs +++ b/crates/openshell-ocsf/src/events/process_activity.rs @@ -5,11 +5,12 @@ use serde::{Deserialize, Serialize}; +use crate::enums::{ActionId, DispositionId, LaunchTypeId}; use crate::events::base_event::BaseEventData; use crate::objects::{Actor, Process}; /// OCSF Process Activity Event [1007]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct ProcessActivityEvent { /// Common base event fields. #[serde(flatten)] @@ -22,39 +23,55 @@ pub struct ProcessActivityEvent { #[serde(skip_serializing_if = "Option::is_none")] pub actor: Option, - /// Launch type ID (new in v1.7.0). - #[serde(skip_serializing_if = "Option::is_none")] - pub launch_type_id: Option, - - /// Launch type label. - #[serde(skip_serializing_if = "Option::is_none")] - pub launch_type: Option, + /// Launch type. + #[serde( + rename = "launch_type_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub launch_type: Option, /// Process exit code (for Terminate activity). #[serde(skip_serializing_if = "Option::is_none")] pub exit_code: Option, - /// Action ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub action_id: Option, + /// Action (Security Control profile). + #[serde(rename = "action_id", default, skip_serializing_if = "Option::is_none")] + pub action: Option, + + /// Disposition. + #[serde( + rename = "disposition_id", + default, + skip_serializing_if = "Option::is_none" + )] + pub disposition: Option, +} - /// Action label. - #[serde(skip_serializing_if = "Option::is_none")] - pub action: Option, +impl Serialize for ProcessActivityEvent { + fn serialize(&self, serializer: S) -> Result { + use crate::events::serde_helpers::{insert_enum_pair, insert_optional, insert_required}; - /// Disposition ID. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition_id: Option, + let mut base_val = serde_json::to_value(&self.base).map_err(serde::ser::Error::custom)?; + let obj = base_val + .as_object_mut() + .ok_or_else(|| serde::ser::Error::custom("expected object"))?; - /// Disposition label. - #[serde(skip_serializing_if = "Option::is_none")] - pub disposition: Option, + insert_required!(obj, "process", self.process); + insert_optional!(obj, "actor", self.actor); + insert_enum_pair!(obj, "launch_type", self.launch_type); + insert_optional!(obj, "exit_code", self.exit_code); + insert_enum_pair!(obj, "action", self.action); + insert_enum_pair!(obj, "disposition", self.disposition); + + base_val.serialize(serializer) + } } #[cfg(test)] mod tests { use super::*; - use crate::enums::SeverityId; + use crate::enums::{ActionId, DispositionId, LaunchTypeId, SeverityId}; use crate::objects::{Metadata, Product}; #[test] @@ -80,13 +97,10 @@ mod tests { actor: Some(Actor { process: Process::new("openshell-sandbox", 1), }), - launch_type_id: Some(1), - launch_type: Some("Spawn".to_string()), + launch_type: Some(LaunchTypeId::Spawn), exit_code: None, - action_id: Some(1), - action: Some("Allowed".to_string()), - disposition_id: Some(1), - disposition: Some("Allowed".to_string()), + action: Some(ActionId::Allowed), + disposition: Some(DispositionId::Allowed), }; let json = serde_json::to_value(&event).unwrap(); diff --git a/crates/openshell-ocsf/src/events/serde_helpers.rs b/crates/openshell-ocsf/src/events/serde_helpers.rs new file mode 100644 index 00000000..d7881ada --- /dev/null +++ b/crates/openshell-ocsf/src/events/serde_helpers.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Serialization helpers for OCSF event structs. +//! +//! These macros reduce boilerplate in custom `Serialize` impls that expand +//! typed enum fields into OCSF's `_id` + label pair format. + +/// Insert an OCSF enum pair (`_id` integer + label string) into a JSON map. +/// +/// If the value is `Some`, inserts both `"_id": ` and `"": "