Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions architecture/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ HTTP Activity records contain a request or response; early rejections with only
connection context use Network Activity. Producer regression tests validate
required fields and `at_least_one` constraints against the vendored OCSF 1.8
schemas.
Network Activity records identify at least one observed endpoint; connection
failures retain their known peer or listening endpoint.
Configuration diagnostics use Config State Change.
Unix socket relay and relay-control notifications, plus proxy and mediation
failures without an observed network endpoint, use Base Event.

## Policy Proposals

Expand Down
14 changes: 0 additions & 14 deletions crates/openshell-ocsf/src/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,20 +134,6 @@ macro_rules! impl_src_endpoint_addr_setter {
};
}

/// Generate the `firewall_rule` setter shared by network and HTTP builders.
macro_rules! impl_firewall_rule_setter {
($builder:ident) => {
impl<'a> $builder<'a> {
/// Set the firewall rule that matched this event.
#[must_use]
pub fn firewall_rule(mut self, name: &str, rule_type: &str) -> Self {
self.firewall_rule = Some($crate::objects::FirewallRule::new(name, rule_type));
self
}
}
};
}

mod api_activity;
mod base;
mod config;
Expand Down
173 changes: 163 additions & 10 deletions crates/openshell-ocsf/src/builders/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::events::{NetworkActivityEvent, OcsfEvent};
use crate::objects::{Actor, ConnectionInfo, Endpoint, FirewallRule};

/// Builder for Network Activity [4001] events.
pub struct NetworkActivityBuilder<'a> {
pub struct NetworkActivityBuilder<'a, EndpointState = MissingNetworkEndpoint> {
ctx: &'a EventContext,
activity: ActivityId,
activity_name: Option<String>,
Expand All @@ -28,10 +28,35 @@ pub struct NetworkActivityBuilder<'a> {
status_detail: Option<String>,
unmapped: Option<serde_json::Map<String, serde_json::Value>>,
log_source: Option<String>,
endpoint_state: std::marker::PhantomData<EndpointState>,
}

impl<'a> NetworkActivityBuilder<'a> {
/// Marker for a Network Activity builder without an endpoint.
pub struct MissingNetworkEndpoint;

/// Marker for a Network Activity builder with a source or destination endpoint.
pub struct HasNetworkEndpoint;

impl<'a> NetworkActivityBuilder<'a, MissingNetworkEndpoint> {
/// Start building a Network Activity event.
///
/// A Network Activity must include a source or destination endpoint before
/// it can be built.
///
/// ```compile_fail
/// use openshell_ocsf::{EventContext, NetworkActivityBuilder};
///
/// let ctx = EventContext {
/// sandbox_id: String::new(),
/// sandbox_name: String::new(),
/// container_image: String::new(),
/// hostname: String::new(),
/// product_version: String::new(),
/// proxy_ip: "127.0.0.1".parse().unwrap(),
/// proxy_port: 3128,
/// };
/// NetworkActivityBuilder::new(&ctx).build();
/// ```
#[must_use]
pub fn new(ctx: &'a EventContext) -> Self {
Self {
Expand All @@ -52,6 +77,92 @@ impl<'a> NetworkActivityBuilder<'a> {
status_detail: None,
unmapped: None,
log_source: None,
endpoint_state: std::marker::PhantomData,
}
}

#[must_use]
pub fn src_endpoint_addr(
self,
ip: std::net::IpAddr,
port: u16,
) -> NetworkActivityBuilder<'a, HasNetworkEndpoint> {
self.with_src_endpoint(Endpoint::from_ip(ip, port))
}

#[must_use]
pub fn dst_endpoint(
self,
endpoint: Endpoint,
) -> NetworkActivityBuilder<'a, HasNetworkEndpoint> {
self.with_dst_endpoint(endpoint)
}
}

impl NetworkActivityBuilder<'_, HasNetworkEndpoint> {
#[must_use]
pub fn src_endpoint_addr(mut self, ip: std::net::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
}
}

impl<'a, EndpointState> NetworkActivityBuilder<'a, EndpointState> {
fn with_src_endpoint(
self,
endpoint: Endpoint,
) -> NetworkActivityBuilder<'a, HasNetworkEndpoint> {
NetworkActivityBuilder {
ctx: self.ctx,
activity: self.activity,
activity_name: self.activity_name,
action: self.action,
disposition: self.disposition,
severity: self.severity,
status: self.status,
src_endpoint: Some(endpoint),
dst_endpoint: self.dst_endpoint,
actor: self.actor,
firewall_rule: self.firewall_rule,
connection_info: self.connection_info,
observation_point_id: self.observation_point_id,
message: self.message,
status_detail: self.status_detail,
unmapped: self.unmapped,
log_source: self.log_source,
endpoint_state: std::marker::PhantomData,
}
}

fn with_dst_endpoint(
self,
endpoint: Endpoint,
) -> NetworkActivityBuilder<'a, HasNetworkEndpoint> {
NetworkActivityBuilder {
ctx: self.ctx,
activity: self.activity,
activity_name: self.activity_name,
action: self.action,
disposition: self.disposition,
severity: self.severity,
status: self.status,
src_endpoint: self.src_endpoint,
dst_endpoint: Some(endpoint),
actor: self.actor,
firewall_rule: self.firewall_rule,
connection_info: self.connection_info,
observation_point_id: self.observation_point_id,
message: self.message,
status_detail: self.status_detail,
unmapped: self.unmapped,
log_source: self.log_source,
endpoint_state: std::marker::PhantomData,
}
}

Expand Down Expand Up @@ -90,6 +201,56 @@ impl<'a> NetworkActivityBuilder<'a> {
self
}

#[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 actor_process(mut self, process: crate::objects::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 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<String>) -> Self {
self.message = Some(msg.into());
self
}
}

impl NetworkActivityBuilder<'_, HasNetworkEndpoint> {
/// Finalize and return the `OcsfEvent`.
#[must_use]
pub fn build(self) -> OcsfEvent {
Expand Down Expand Up @@ -139,14 +300,6 @@ impl<'a> NetworkActivityBuilder<'a> {
}
}

impl_activity_setter!(NetworkActivityBuilder);
impl_action_disposition_setters!(NetworkActivityBuilder);
impl_actor_process_setter!(NetworkActivityBuilder);
impl_dst_endpoint_setter!(NetworkActivityBuilder);
impl_src_endpoint_addr_setter!(NetworkActivityBuilder);
impl_firewall_rule_setter!(NetworkActivityBuilder);
impl_builder_setters!(NetworkActivityBuilder);

#[cfg(test)]
mod tests {
use super::*;
Expand Down
5 changes: 4 additions & 1 deletion crates/openshell-ocsf/tests/event_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

use std::net::{IpAddr, Ipv4Addr};

use openshell_ocsf::{ActivityId, EventContext, NetworkActivityBuilder, OcsfEvent, SeverityId};
use openshell_ocsf::{
ActivityId, Endpoint, EventContext, NetworkActivityBuilder, OcsfEvent, SeverityId,
};

fn sandbox_ctx(container_image: &str) -> EventContext {
EventContext {
Expand All @@ -23,6 +25,7 @@ fn event(ctx: &EventContext) -> OcsfEvent {
NetworkActivityBuilder::new(ctx)
.activity(ActivityId::Open)
.severity(SeverityId::Medium)
.dst_endpoint(Endpoint::from_domain("api.example.com", 443))
.message("CONNECT api.example.com:443")
.build()
}
Expand Down
20 changes: 12 additions & 8 deletions crates/openshell-supervisor-network/src/l7/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,10 @@ pub fn parse_l7_config(val: &regorus::Value) -> Option<L7EndpointConfig> {
let tls = match tls_value.as_str() {
"skip" => TlsMode::Skip,
"terminate" => {
let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
.activity(openshell_ocsf::ActivityId::Other)
let event = openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Medium)
.status(openshell_ocsf::StatusId::Success)
.state(openshell_ocsf::StateId::Other, "deprecated")
.message(
"'tls: terminate' is deprecated; TLS termination is now automatic. \
Use 'tls: skip' to explicitly disable. This field will be removed in a future version.",
Expand All @@ -320,9 +321,10 @@ pub fn parse_l7_config(val: &regorus::Value) -> Option<L7EndpointConfig> {
TlsMode::Auto
}
"passthrough" => {
let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
.activity(openshell_ocsf::ActivityId::Other)
let event = openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::Medium)
.status(openshell_ocsf::StatusId::Success)
.state(openshell_ocsf::StateId::Other, "deprecated")
.message(
"'tls: passthrough' is deprecated; TLS termination is now automatic. \
Use 'tls: skip' to explicitly disable. This field will be removed in a future version.",
Expand Down Expand Up @@ -374,9 +376,10 @@ pub fn parse_l7_config(val: &regorus::Value) -> Option<L7EndpointConfig> {
Some("sigv4:body") => CredentialSigning::SigV4Body,
Some("sigv4:no_body") => CredentialSigning::SigV4NoBody,
Some(other) if !other.is_empty() => {
let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
.activity(openshell_ocsf::ActivityId::Other)
let event = openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::High)
.status(openshell_ocsf::StatusId::Failure)
.state(openshell_ocsf::StateId::Disabled, "invalid")
.message(format!(
"rejecting endpoint: unrecognized credential_signing value {other:?}"
))
Expand All @@ -391,9 +394,10 @@ pub fn parse_l7_config(val: &regorus::Value) -> Option<L7EndpointConfig> {
let signing_region = get_object_str(val, "signing_region").unwrap_or_default();

if credential_signing.is_sigv4() && signing_service.is_empty() {
let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx())
.activity(openshell_ocsf::ActivityId::Other)
let event = openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx())
.severity(openshell_ocsf::SeverityId::High)
.status(openshell_ocsf::StatusId::Failure)
.state(openshell_ocsf::StateId::Disabled, "invalid")
.message("rejecting endpoint: credential_signing requires signing_service".to_string())
.build();
openshell_ocsf::ocsf_emit!(event);
Expand Down
Loading
Loading