You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The sandbox supervisor emits 123 log statements across 18 source files using ad-hoc tracing::info!()/warn!() macros with inconsistent field names, no event classification, and no machine-readable output. There is no schema governing the log events, making it impossible to reliably filter, correlate, alert on, or export them to SIEMs.
We need a dedicated crate that implements the OCSF v1.7.0 event model for all sandbox log events, providing typed event structs, dual-format output (human-readable shorthand + JSONL), and schema-validated testing — all independently buildable and testable without modifying the sandbox supervisor.
Proposed Design
Create a new openshell-ocsf crate at crates/openshell-ocsf/ that owns all OCSF logic. The sandbox (Part 2, separate issue) will later depend on this crate and use its builders to construct events. This issue covers everything that can be built and tested standalone.
The full design is documented in .opencode/plans/ocsf-log-export.md. Key sections: "The openshell-ocsf Crate", "Shorthand Format Design", "Event Class Mapping", "Vendored Schema for Test Validation".
Crate Scope
8 OCSF event classes: 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]
Create schemas/ocsf/README.md documenting provenance, fetch date, and upgrade procedure
Create mise task ocsf:update-schema that re-fetches schemas for a given version
Commit all schema JSON files to the repository
Done when: All 25 schema JSON files exist, are valid JSON, and VERSION file reads 1.7.0. The mise run ocsf:update-schema -- 1.7.0 task runs successfully and produces identical output.
Add unit tests loading each vendored class schema and verifying structure is parseable
Done when: cargo test -p openshell-ocsf validation passes. Each of the 8 class schemas loads successfully. validate_required_fields correctly identifies missing required fields in a synthetic event. validate_enum_value correctly rejects invalid enum values.
Depends on: Steps 1, 2.
Step 4: Core types — enums (~1 day)
Implement all OCSF enum types with Serialize/Deserialize derives and integer representation:
Each enum serializes to its integer value in JSON and has fn label(&self) -> &str returning the OCSF string label
Done when: All enum types compile, serialize to correct integer values, and each enum value validates against the corresponding vendored schema enum definition. Unit tests cover every variant of every enum.
Depends on: Steps 1, 3 (uses validation utilities in tests).
Step 5: Core types — objects (~1-1.5 days)
Implement all OCSF object types with Serialize/Deserialize:
Metadata, Product — with profiles array, uid, version, log_source
All fields use #[serde(skip_serializing_if = "Option::is_none")] for optional OCSF fields
Done when: All object types compile, serialize to correct JSON structure, and unit tests verify field names match the vendored object schemas. Each object has at least one construction + serialization test.
Depends on: Steps 1, 4 (objects reference enums).
Step 6: Event structs (~1-1.5 days)
Implement BaseEventData with all OCSF base event fields (class_uid, class_name, category_uid, category_name, activity_id, activity_name, type_uid, type_name, time, severity_id, severity, status_id, status, message, metadata, device, container, unmapped)
Implement all 8 event class structs, each embedding BaseEventData via #[serde(flatten)]:
Done when: All 8 event structs compile and serialize to JSON with correct class_uid, category_uid, type_uid, and type_name. At least one test per class verifies the serialized JSON validates against the vendored class schema (required fields present, enum values valid).
Depends on: Steps 4, 5 (events reference enums and objects).
Step 7: JSONL serializer (~0.5-1 day)
Implement to_json(&self) -> serde_json::Value and to_json_line(&self) -> String on OcsfEvent
to_json() returns the full OCSF JSON object
to_json_line() returns to_json() serialized as a single line (no pretty-printing) with trailing newline
Ensure #[serde(skip_serializing_if)] is correctly applied so absent optional fields are omitted (not null)
Done when: Every event class has at least one test that: (a) serializes to JSON via to_json(), (b) validates against the vendored schema with validate_required_fields(), (c) validates all enum fields with validate_enum_value(), (d) verifies to_json_line() is a single line ending in \n and parses back to the same JSON value.
Depends on: Steps 3, 6 (uses validation utilities against event structs).
Step 8: Shorthand formatter (~1 day)
Implement format_shorthand(&self) -> String on OcsfEvent with per-class templates:
Implement format_ts(time_ms: i64) -> String — ISO 8601 compact
Implement severity_char(severity_id: SeverityId) -> char — I, L, M, H, C, F,
Add snapshot tests (using insta or inline expected strings) for every class variant. At least 2 snapshots per class (common case + edge case)
Done when: format_shorthand() produces correct output for all 8 event classes. At least 16 snapshot tests pass (2 per class). Shorthand output is deterministic (same input → same output).
Depends on: Step 6 (formats event structs).
Step 9: Round-trip tests (~0.5 day)
For each event class, verify consistency between shorthand and JSON representations:
Shorthand class prefix (NET, HTTP, SSH, etc.) matches JSON class_uid
Each builder's .build() returns OcsfEvent. Builders auto-populate time, metadata, container, device from SandboxContext
Done when: All 8 builders compile and produce valid OcsfEvent instances. Each builder has at least one test that builds an event and validates it against the vendored schema. Builder ergonomics match the "Before and After" examples in the plan.
Depends on: Steps 4, 5, 6 (builders construct events from types).
Implement tracing/event_bridge.rs: emit_ocsf_event(event: OcsfEvent) function emitting with target ocsf, plus ocsf_emit!($event) macro
Implement tracing/shorthand_layer.rs: OcsfShorthandLayer — a tracing::Layer that intercepts ocsf target events, calls format_shorthand(), writes to provided writer. Non-OCSF events pass through with fallback format
Implement tracing/jsonl_layer.rs: OcsfJsonlLayer — a tracing::Layer that intercepts ocsf target events, calls to_json_line(), writes to provided writer
Add unit tests with mock writers (Vec<u8>) verifying:
An ocsf_emit! call results in both layers receiving the event
Done when: Both layers correctly format OCSF events. ocsf_emit! macro works. Mock-writer tests pass for at least 3 event classes. Non-OCSF event fallback test passes.
Depends on: Steps 7, 8, 10 (layers use formatters and builders).
Step 12: CI integration (~0.5 day)
Ensure cargo test -p openshell-ocsf passes in CI with all tests green
Add CI check that vendored VERSION file matches OCSF_VERSION constant in Rust code
Run mise run pre-commit and fix any lint, format, or license header issues
Verify cargo clippy -p openshell-ocsf has zero warnings
Done when: CI green. mise run pre-commit passes. cargo test -p openshell-ocsf runs all tests with zero failures. Vendored schema version matches code constants.
Depends on: All prior steps.
Acceptance Criteria
cargo check -p openshell-ocsf compiles with zero errors and zero warnings
cargo test -p openshell-ocsf passes with all tests green (target: 80+ tests covering all 8 event classes, all formatters, all builders, schema validation, round-trip consistency)
cargo clippy -p openshell-ocsf has zero warnings
mise run pre-commit passes
Every event class has at least one JSON serialization test validating against the vendored OCSF v1.7.0 schema
Every event class has at least two shorthand format snapshot tests
Dual-emit events (BYPASS_DETECT, NSSH1 replay) have round-trip consistency tests
Both tracing layers (OcsfShorthandLayer, OcsfJsonlLayer) have mock-writer tests demonstrating correct output
The ocsf_emit! macro compiles and correctly routes events to both layers
No code in openshell-sandbox has been modified — the crate is fully standalone
Problem Statement
The sandbox supervisor emits 123 log statements across 18 source files using ad-hoc
tracing::info!()/warn!()macros with inconsistent field names, no event classification, and no machine-readable output. There is no schema governing the log events, making it impossible to reliably filter, correlate, alert on, or export them to SIEMs.We need a dedicated crate that implements the OCSF v1.7.0 event model for all sandbox log events, providing typed event structs, dual-format output (human-readable shorthand + JSONL), and schema-validated testing — all independently buildable and testable without modifying the sandbox supervisor.
Proposed Design
Create a new
openshell-ocsfcrate atcrates/openshell-ocsf/that owns all OCSF logic. The sandbox (Part 2, separate issue) will later depend on this crate and use its builders to construct events. This issue covers everything that can be built and tested standalone.The full design is documented in
.opencode/plans/ocsf-log-export.md. Key sections: "Theopenshell-ocsfCrate", "Shorthand Format Design", "Event Class Mapping", "Vendored Schema for Test Validation".Crate Scope
SeverityId,StatusId,ActionId,DispositionId,ActivityId,StateId,AuthTypeId,LaunchTypeId,SecurityLevelId,ConfidenceId,RiskLevelIdMetadata,Product,Endpoint,Process,Actor,Container,Image,Device,OsInfo,FirewallRule,FindingInfo,Evidence,Remediation,HttpRequest,HttpResponse,Url,Attack,Technique,Tactic,ConnectionInfoSandboxContextfor shared metadataformat_shorthand()(single-line human-readable) andto_json()/to_json_line()(OCSF JSONL)OcsfShorthandLayerandOcsfJsonlLayerfor subscriber integrationocsf_emit!macro: Thin wrapper for emitting events through the tracing systemModule Structure
Shorthand Format
Single-line human-readable format derived from OCSF events:
Examples:
Order of Battle
Each step depends on prior steps unless noted. No
openshell-sandboxcode is modified in this issue.Step 1: Crate scaffolding (~0.5 day)
crates/openshell-ocsf/directory with full module structure (empty files withmoddeclarations)Cargo.tomlwith dependencies:serde,serde_json,tracing,tracing-subscriber,chrono(all already in workspace)openshell-ocsfto workspaceCargo.tomlmembers listsrc/lib.rswith crate-level docs and placeholder re-exportsevents/mod.rs,objects/mod.rs,enums/mod.rs,builders/mod.rs,format/mod.rs,tracing/mod.rs,validation/mod.rsDone when:
cargo check -p openshell-ocsfcompiles with zero errors and zero warnings. No functional code yet.Step 2: Vendor OCSF schemas (~0.5 day)
schema.ocsf.io/api/1.7.0/crates/openshell-ocsf/schemas/ocsf/v1.7.0/classes/andobjects/schemas/ocsf/v1.7.0/VERSIONcontaining1.7.0schemas/ocsf/README.mddocumenting provenance, fetch date, and upgrade proceduremisetaskocsf:update-schemathat re-fetches schemas for a given versionDone when: All 25 schema JSON files exist, are valid JSON, and
VERSIONfile reads1.7.0. Themise run ocsf:update-schema -- 1.7.0task runs successfully and produces identical output.Schema files to vendor:
Classes (8):
network_activity,http_activity,ssh_activity,process_activity,detection_finding,application_lifecycle,device_config_state_change,base_eventObjects (17):
metadata,network_endpoint,network_proxy,process,actor,device,container,product,firewall_rule,finding_info,evidences,http_request,http_response,url,attack,remediation,connection_infoStep 3: Schema validation utilities (~0.5 day)
validation/schema.rswith:load_class_schema(class: &str) -> Value— loads vendored class schema by namevalidate_required_fields(event: &Value, schema: &Value)— asserts all required fields presentvalidate_enum_value(event: &Value, field: &str, schema: &Value)— asserts enum values are valid#[cfg(test)]— test-only utilitiesDone when:
cargo test -p openshell-ocsf validationpasses. Each of the 8 class schemas loads successfully.validate_required_fieldscorrectly identifies missing required fields in a synthetic event.validate_enum_valuecorrectly rejects invalid enum values.Depends on: Steps 1, 2.
Step 4: Core types — enums (~1 day)
Serialize/Deserializederives and integer representation:SeverityId(0-6, 99) — Unknown, Informational, Low, Medium, High, Critical, Fatal, OtherStatusId(0-2, 99) — Unknown, Success, Failure, OtherActionId(0-4, 99) — Unknown, Allowed, Denied, ...DispositionId(0-27, 99) — Unknown, Allowed, Blocked, ... Error, ...ActivityId— per-class variants (separate enum types or unified with class context)StateId(0-2, 99) — Unknown, Disabled, Enabled, OtherAuthTypeId(0-6, 99) — Unknown, Certificate Based, GSSAPI, Host Based, Keyboard Interactive, Password, Public Key, OtherLaunchTypeId(0-3, 99) — Unknown, Spawn, Fork, Exec, OtherSecurityLevelId(0-3, 99) — Unknown, Secure, At Risk, Compromised, OtherConfidenceId(0-3, 99) — Unknown, Low, Medium, High, OtherRiskLevelId(0-4, 99) — Unknown, Info, Low, Medium, High, Critical, Otherfn label(&self) -> &strreturning the OCSF string labelDone when: All enum types compile, serialize to correct integer values, and each enum value validates against the corresponding vendored schema enum definition. Unit tests cover every variant of every enum.
Depends on: Steps 1, 3 (uses validation utilities in tests).
Step 5: Core types — objects (~1-1.5 days)
Serialize/Deserialize:Metadata,Product— withprofilesarray,uid,version,log_sourceEndpoint— withfn domain(name, port),fn ip(addr, port),fn domain_or_ip(&self) -> StringProcess,Actor—Processhas optionalparent_process: Box<Option<Process>>for ancestor chainContainer,ImageDevice,OsInfoFirewallRule—name,typeFindingInfo,Evidence,Remediation—FindingInfohasuid,title,desc;RemediationhasdescHttpRequest,HttpResponse,Url—HttpRequesthashttp_method,url;Urlhasscheme,hostname,path,portAttack,Technique,Tactic— withAttack::mitre(technique_uid, name, tactic_uid, name)convenience constructorConnectionInfo—protocol_name#[serde(skip_serializing_if = "Option::is_none")]for optional OCSF fieldsDone when: All object types compile, serialize to correct JSON structure, and unit tests verify field names match the vendored object schemas. Each object has at least one construction + serialization test.
Depends on: Steps 1, 4 (objects reference enums).
Step 6: Event structs (~1-1.5 days)
BaseEventDatawith all OCSF base event fields (class_uid,class_name,category_uid,category_name,activity_id,activity_name,type_uid,type_name,time,severity_id,severity,status_id,status,message,metadata,device,container,unmapped)BaseEventDatavia#[serde(flatten)]:NetworkActivityEvent[4001] — addssrc_endpoint,dst_endpoint,proxy_endpoint,actor,firewall_rule,connection_info,action_id,action,disposition_id,disposition,observation_point_id,is_src_dst_assignment_knownHttpActivityEvent[4002] — addshttp_request,http_response,src_endpoint,dst_endpoint,proxy_endpoint,actor,firewall_ruleSshActivityEvent[4007] — addssrc_endpoint,dst_endpoint,auth_type_id,auth_type,protocol_ver,actorProcessActivityEvent[1007] — addsprocess,actor,launch_type_id,launch_type,exit_codeDetectionFindingEvent[2004] — addsfinding_info,evidences,attacks,remediation,is_alert,confidence_id,confidence,risk_level_id,risk_levelApplicationLifecycleEvent[6002] — addsapp(Product)DeviceConfigStateChangeEvent[5019] — addsstate_id,state,security_level_id,security_level,prev_security_level_id,prev_security_levelBaseEvent[0] — justBaseEventDataOcsfEventenum with variants for all 8 classestype_uidauto-computation:class_uid * 100 + activity_idDone when: All 8 event structs compile and serialize to JSON with correct
class_uid,category_uid,type_uid, andtype_name. At least one test per class verifies the serialized JSON validates against the vendored class schema (required fields present, enum values valid).Depends on: Steps 4, 5 (events reference enums and objects).
Step 7: JSONL serializer (~0.5-1 day)
to_json(&self) -> serde_json::Valueandto_json_line(&self) -> StringonOcsfEventto_json()returns the full OCSF JSON objectto_json_line()returnsto_json()serialized as a single line (no pretty-printing) with trailing newline#[serde(skip_serializing_if)]is correctly applied so absent optional fields are omitted (notnull)Done when: Every event class has at least one test that: (a) serializes to JSON via
to_json(), (b) validates against the vendored schema withvalidate_required_fields(), (c) validates all enum fields withvalidate_enum_value(), (d) verifiesto_json_line()is a single line ending in\nand parses back to the same JSON value.Depends on: Steps 3, 6 (uses validation utilities against event structs).
Step 8: Shorthand formatter (~1 day)
format_shorthand(&self) -> StringonOcsfEventwith per-class templates:NET:<activity> <action> <process>(<pid>) -> <dst>:<port> [policy:<rule> engine:<engine>]HTTP:<method> <action> <process>(<pid>) -> <method> <url> [policy:<rule>]SSH:<activity> <action> <peer> [auth:<auth_type>]PROC:<activity> <process>(<pid>) [exit:<code>] [cmd:<cmdline>]FINDING:<disposition> "<title>" [confidence:<level>]LIFECYCLE:<activity> <app> <status>CONFIG:<state> <what> [version:<ver> hash:<hash>]EVENT <message> [<key fields>]format_ts(time_ms: i64) -> String— ISO 8601 compactseverity_char(severity_id: SeverityId) -> char—I,L,M,H,C,F,instaor inline expected strings) for every class variant. At least 2 snapshots per class (common case + edge case)Done when:
format_shorthand()produces correct output for all 8 event classes. At least 16 snapshot tests pass (2 per class). Shorthand output is deterministic (same input → same output).Depends on: Step 6 (formats event structs).
Step 9: Round-trip tests (~0.5 day)
NET,HTTP,SSH, etc.) matches JSONclass_uidOPEN,GET,LAUNCH, etc.) matches JSONactivity_nameALLOW,DENY, etc.) matches JSONaction(when present)severity_idDone when: At least one round-trip test per event class passes. Dual-emit consistency tests for BYPASS_DETECT and NSSH1 replay pass.
Depends on: Steps 7, 8 (uses both formatters).
Step 10:
SandboxContext+ Builders (~1-1.5 days)SandboxContextstruct withsandbox_id,sandbox_name,container_image,hostname,product_version,proxy_ip,proxy_portmetadata(),container(),device(),proxy_endpoint()methods onSandboxContextNetworkActivityBuilder— required:activity,action,disposition,severity,dst_endpoint. Optional:src_endpoint,actor_process,firewall_rule,message,status,connection_info,observation_pointHttpActivityBuilder— required:activity(HTTP method),action,disposition,severity,http_request. Optional:http_response,src_endpoint,dst_endpoint,actor_process,firewall_rule,messageSshActivityBuilder— required:activity,action,disposition,severity. Optional:src_endpoint,dst_endpoint,auth_type,protocol_ver,messageProcessActivityBuilder— required:activity,severity,process. Optional:action,disposition,launch_type,actor_process,exit_code,messageDetectionFindingBuilder— required:activity,severity,finding_info. Optional:action,disposition,is_alert,confidence,risk_level,evidences,attacks,remediation,messageAppLifecycleBuilder— required:activity,severity,status. Optional:messageConfigStateChangeBuilder— required:state,severity. Optional:security_level,prev_security_level,status,unmapped,messageBaseEventBuilder— required:severity,message. Optional:unmapped.build()returnsOcsfEvent. Builders auto-populatetime,metadata,container,devicefromSandboxContextDone when: All 8 builders compile and produce valid
OcsfEventinstances. Each builder has at least one test that builds an event and validates it against the vendored schema. Builder ergonomics match the "Before and After" examples in the plan.Depends on: Steps 4, 5, 6 (builders construct events from types).
Step 11:
ocsf_emit!macro + tracing layers (~1-1.5 days)tracing/event_bridge.rs:emit_ocsf_event(event: OcsfEvent)function emitting with targetocsf, plusocsf_emit!($event)macrotracing/shorthand_layer.rs:OcsfShorthandLayer— atracing::Layerthat interceptsocsftarget events, callsformat_shorthand(), writes to provided writer. Non-OCSF events pass through with fallback formattracing/jsonl_layer.rs:OcsfJsonlLayer— atracing::Layerthat interceptsocsftarget events, callsto_json_line(), writes to provided writerVec<u8>) verifying:ocsf_emit!call results in both layers receiving the eventDone when: Both layers correctly format OCSF events.
ocsf_emit!macro works. Mock-writer tests pass for at least 3 event classes. Non-OCSF event fallback test passes.Depends on: Steps 7, 8, 10 (layers use formatters and builders).
Step 12: CI integration (~0.5 day)
cargo test -p openshell-ocsfpasses in CI with all tests greenVERSIONfile matchesOCSF_VERSIONconstant in Rust codemise run pre-commitand fix any lint, format, or license header issuescargo clippy -p openshell-ocsfhas zero warningsDone when: CI green.
mise run pre-commitpasses.cargo test -p openshell-ocsfruns all tests with zero failures. Vendored schema version matches code constants.Depends on: All prior steps.
Acceptance Criteria
cargo check -p openshell-ocsfcompiles with zero errors and zero warningscargo test -p openshell-ocsfpasses with all tests green (target: 80+ tests covering all 8 event classes, all formatters, all builders, schema validation, round-trip consistency)cargo clippy -p openshell-ocsfhas zero warningsmise run pre-commitpassesOcsfShorthandLayer,OcsfJsonlLayer) have mock-writer tests demonstrating correct outputocsf_emit!macro compiles and correctly routes events to both layersopenshell-sandboxhas been modified — the crate is fully standaloneEstimated Effort
~8-10 days
References
.opencode/plans/ocsf-log-export.md