diff --git a/crates/openshell-core/src/driver_utils.rs b/crates/openshell-core/src/driver_utils.rs index 045acb398..4438fbbe4 100644 --- a/crates/openshell-core/src/driver_utils.rs +++ b/crates/openshell-core/src/driver_utils.rs @@ -3,7 +3,9 @@ //! Utility helpers shared across compute-driver crates. -use crate::proto::compute::v1::DriverSandbox; +use std::path::PathBuf; + +use crate::proto::compute::v1::{DriverSandbox, GetCapabilitiesResponse}; // --------------------------------------------------------------------------- // Sandbox container/pod label keys (openshell.ai/ namespace) @@ -34,6 +36,50 @@ pub const LABEL_SANDBOX_NAMESPACE: &str = "openshell.ai/sandbox-namespace"; /// path used when building the `openshell-sandbox` image layer. pub const SUPERVISOR_IMAGE_BINARY_PATH: &str = "/openshell-sandbox"; +/// Return the XDG state path for a driver's sandbox JWT token file. +/// +/// The resulting path is `$XDG_STATE_HOME/openshell/[/]//sandbox.jwt`. +/// +/// `driver_subdir` is driver-specific, e.g. `"docker-sandbox-tokens"` or +/// `"podman-sandbox-tokens"`. When `namespace` is `Some`, it is appended as +/// an additional path component (with `/` and `\` replaced by `-`). +/// +/// # Errors +/// Returns an error if the XDG state directory cannot be resolved. +pub fn sandbox_token_path( + driver_subdir: &str, + namespace: Option<&str>, + sandbox_id: &str, +) -> miette::Result { + let mut path = crate::paths::xdg_state_dir()? + .join("openshell") + .join(driver_subdir); + if let Some(ns) = namespace { + path = path.join(ns.replace(['/', '\\'], "-")); + } + Ok(path.join(sandbox_id).join("sandbox.jwt")) +} + +/// Build a [`GetCapabilitiesResponse`] from the common driver capability fields. +/// +/// Every compute driver constructs this response with the same fields. Shared +/// here to avoid repeating the struct literal (and the always-zero `gpu_count` +/// default) in each driver crate. +pub fn build_capabilities_response( + driver_name: &str, + driver_version: impl Into, + default_image: impl Into, + supports_gpu: bool, +) -> GetCapabilitiesResponse { + GetCapabilitiesResponse { + driver_name: driver_name.to_string(), + driver_version: driver_version.into(), + default_image: default_image.into(), + supports_gpu, + gpu_count: 0, + } +} + /// Return the effective log level for a sandbox. /// /// Uses the level from the sandbox spec when non-empty, falling back to diff --git a/crates/openshell-driver-docker/src/lib.rs b/crates/openshell-driver-docker/src/lib.rs index 010b8a042..005226d65 100644 --- a/crates/openshell-driver-docker/src/lib.rs +++ b/crates/openshell-driver-docker/src/lib.rs @@ -320,13 +320,12 @@ impl DockerComputeDriver { } fn capabilities(&self) -> GetCapabilitiesResponse { - GetCapabilitiesResponse { - driver_name: "docker".to_string(), - driver_version: self.config.daemon_version.clone(), - default_image: self.config.default_image.clone(), - supports_gpu: self.config.supports_gpu, - gpu_count: 0, - } + openshell_core::driver_utils::build_capabilities_response( + "docker", + &self.config.daemon_version, + &self.config.default_image, + self.config.supports_gpu, + ) } fn validate_sandbox( @@ -962,17 +961,16 @@ fn sandbox_token_host_path_by_id( sandbox_id: &str, config: &DockerDriverRuntimeConfig, ) -> Result { - let base = openshell_core::paths::xdg_state_dir().map_err(|err| { + openshell_core::driver_utils::sandbox_token_path( + "docker-sandbox-tokens", + Some(&config.sandbox_namespace), + sandbox_id, + ) + .map_err(|err| { Status::internal(format!( "resolve sandbox token state directory failed: {err}" )) - })?; - Ok(base - .join("openshell") - .join("docker-sandbox-tokens") - .join(config.sandbox_namespace.replace(['/', '\\'], "-")) - .join(sandbox_id) - .join("sandbox.jwt")) + }) } async fn write_sandbox_token_file( diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index 41be9edc0..0a428f146 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -160,13 +160,12 @@ impl KubernetesComputeDriver { } pub async fn capabilities(&self) -> Result { - Ok(GetCapabilitiesResponse { - driver_name: "kubernetes".to_string(), - driver_version: openshell_core::VERSION.to_string(), - default_image: self.config.default_image.clone(), - supports_gpu: self.has_gpu_capacity().await.unwrap_or(false), - gpu_count: 0, - }) + Ok(openshell_core::driver_utils::build_capabilities_response( + "kubernetes", + openshell_core::VERSION, + &self.config.default_image, + self.has_gpu_capacity().await.unwrap_or(false), + )) } pub fn default_image(&self) -> &str { diff --git a/crates/openshell-driver-podman/src/driver.rs b/crates/openshell-driver-podman/src/driver.rs index ec8c519ee..a39379bb3 100644 --- a/crates/openshell-driver-podman/src/driver.rs +++ b/crates/openshell-driver-podman/src/driver.rs @@ -57,13 +57,8 @@ fn validated_container_name(sandbox_name: &str) -> Result Result { - let base = openshell_core::paths::xdg_state_dir() - .map_err(|err| ComputeDriverError::Message(format!("resolve state dir failed: {err}")))?; - Ok(base - .join("openshell") - .join("podman-sandbox-tokens") - .join(sandbox_id) - .join("sandbox.jwt")) + openshell_core::driver_utils::sandbox_token_path("podman-sandbox-tokens", None, sandbox_id) + .map_err(|err| ComputeDriverError::Message(format!("resolve state dir failed: {err}"))) } async fn write_sandbox_token_file( @@ -257,14 +252,12 @@ impl PodmanComputeDriver { /// Report driver capabilities. pub fn capabilities(&self) -> Result { - let supports_gpu = Self::has_gpu_capacity(); - Ok(GetCapabilitiesResponse { - driver_name: "podman".to_string(), - driver_version: openshell_core::VERSION.to_string(), - default_image: self.config.default_image.clone(), - supports_gpu, - gpu_count: 0, - }) + Ok(openshell_core::driver_utils::build_capabilities_response( + "podman", + openshell_core::VERSION, + &self.config.default_image, + Self::has_gpu_capacity(), + )) } #[must_use] diff --git a/crates/openshell-ocsf/src/builders/base.rs b/crates/openshell-ocsf/src/builders/base.rs index 791e5a094..ac51f7dd7 100644 --- a/crates/openshell-ocsf/src/builders/base.rs +++ b/crates/openshell-ocsf/src/builders/base.rs @@ -31,21 +31,6 @@ impl<'a> BaseEventBuilder<'a> { } } - #[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()); @@ -72,22 +57,18 @@ impl<'a> BaseEventBuilder<'a> { 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)); } + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::Base(BaseEvent { base }) } } +impl_builder_setters!(BaseEventBuilder); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/config.rs b/crates/openshell-ocsf/src/builders/config.rs index 8ff9cae2f..d8d40dd24 100644 --- a/crates/openshell-ocsf/src/builders/config.rs +++ b/crates/openshell-ocsf/src/builders/config.rs @@ -37,22 +37,6 @@ impl<'a> ConfigStateChangeBuilder<'a> { } } - #[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 { @@ -92,17 +76,11 @@ impl<'a> ConfigStateChangeBuilder<'a> { 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)); } + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::DeviceConfigStateChange(DeviceConfigStateChangeEvent { base, @@ -114,6 +92,8 @@ impl<'a> ConfigStateChangeBuilder<'a> { } } +impl_builder_setters!(ConfigStateChangeBuilder); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/finding.rs b/crates/openshell-ocsf/src/builders/finding.rs index 10d770f46..ed7290ea3 100644 --- a/crates/openshell-ocsf/src/builders/finding.rs +++ b/crates/openshell-ocsf/src/builders/finding.rs @@ -54,11 +54,6 @@ impl<'a> DetectionFindingBuilder<'a> { 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 @@ -89,11 +84,6 @@ impl<'a> DetectionFindingBuilder<'a> { 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 @@ -147,11 +137,7 @@ impl<'a> DetectionFindingBuilder<'a> { 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()); + self.ctx.apply_common_fields(&mut base, None, self.message); OcsfEvent::DetectionFinding(DetectionFindingEvent { base, @@ -178,6 +164,8 @@ impl<'a> DetectionFindingBuilder<'a> { } } +impl_builder_setters!(DetectionFindingBuilder, no_status); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/http.rs b/crates/openshell-ocsf/src/builders/http.rs index 96919f281..0530d26c6 100644 --- a/crates/openshell-ocsf/src/builders/http.rs +++ b/crates/openshell-ocsf/src/builders/http.rs @@ -64,16 +64,6 @@ impl<'a> HttpActivityBuilder<'a> { 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 @@ -104,11 +94,6 @@ impl<'a> HttpActivityBuilder<'a> { 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 @@ -128,17 +113,11 @@ impl<'a> HttpActivityBuilder<'a> { 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); - } if let Some(detail) = self.status_detail { base.set_status_detail(detail); } - base.set_device(self.ctx.device()); - base.set_container(self.ctx.container()); + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::HttpActivity(HttpActivityEvent { base, @@ -157,6 +136,8 @@ impl<'a> HttpActivityBuilder<'a> { } } +impl_builder_setters!(HttpActivityBuilder); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/lifecycle.rs b/crates/openshell-ocsf/src/builders/lifecycle.rs index b0d3a6007..9b5095a7a 100644 --- a/crates/openshell-ocsf/src/builders/lifecycle.rs +++ b/crates/openshell-ocsf/src/builders/lifecycle.rs @@ -35,21 +35,6 @@ impl<'a> AppLifecycleBuilder<'a> { 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 { @@ -64,14 +49,8 @@ impl<'a> AppLifecycleBuilder<'a> { 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()); + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::ApplicationLifecycle(ApplicationLifecycleEvent { base, @@ -80,6 +59,8 @@ impl<'a> AppLifecycleBuilder<'a> { } } +impl_builder_setters!(AppLifecycleBuilder); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/mod.rs b/crates/openshell-ocsf/src/builders/mod.rs index 77004da4a..7f5cc4f9b 100644 --- a/crates/openshell-ocsf/src/builders/mod.rs +++ b/crates/openshell-ocsf/src/builders/mod.rs @@ -6,6 +6,55 @@ //! Each event class has a builder that takes a `SandboxContext` reference //! and provides chainable methods for setting event fields. +/// Generate the shared `severity`, `status`, and `message` setter methods that +/// appear identically on every OCSF event builder in this crate. +/// +/// # Usage +/// ```ignore +/// impl_builder_setters!(MyBuilder); // severity + status + message +/// impl_builder_setters!(MyBuilder, no_status); // severity + message only +/// ``` +macro_rules! impl_builder_setters { + ($builder:ident) => { + impl<'a> $builder<'a> { + /// Set the event severity. + #[must_use] + pub fn severity(mut self, id: $crate::enums::SeverityId) -> Self { + self.severity = id; + self + } + /// Set the overall event status. + #[must_use] + pub fn status(mut self, id: $crate::enums::StatusId) -> Self { + self.status = Some(id); + self + } + /// Set a human-readable event message. + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + } + }; + ($builder:ident, no_status) => { + impl<'a> $builder<'a> { + /// Set the event severity. + #[must_use] + pub fn severity(mut self, id: $crate::enums::SeverityId) -> Self { + self.severity = id; + self + } + /// Set a human-readable event message. + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + } + }; +} + mod base; mod config; mod finding; @@ -27,6 +76,8 @@ pub use ssh::SshActivityBuilder; use std::net::IpAddr; use crate::OCSF_VERSION; +use crate::enums::StatusId; +use crate::events::base_event::BaseEventData; use crate::objects::{Container, Device, Endpoint, Image, Metadata, Product}; /// Immutable context created once at sandbox startup. @@ -87,6 +138,24 @@ impl SandboxContext { pub fn proxy_endpoint(&self) -> Endpoint { Endpoint::from_ip(self.proxy_ip, self.proxy_port) } + + /// Apply the fields common to every builder's `build()` method onto `base`: + /// optional status, optional message, device info, and container info. + pub fn apply_common_fields( + &self, + base: &mut BaseEventData, + status: Option, + message: Option, + ) { + if let Some(s) = status { + base.set_status(s); + } + if let Some(m) = message { + base.set_message(m); + } + base.set_device(self.device()); + base.set_container(self.container()); + } } #[cfg(test)] diff --git a/crates/openshell-ocsf/src/builders/network.rs b/crates/openshell-ocsf/src/builders/network.rs index d0a79925b..42b9bc488 100644 --- a/crates/openshell-ocsf/src/builders/network.rs +++ b/crates/openshell-ocsf/src/builders/network.rs @@ -78,16 +78,6 @@ impl<'a> NetworkActivityBuilder<'a> { 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 @@ -118,11 +108,6 @@ impl<'a> NetworkActivityBuilder<'a> { 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 @@ -166,20 +151,14 @@ impl<'a> NetworkActivityBuilder<'a> { 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)); } + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::NetworkActivity(NetworkActivityEvent { base, @@ -197,6 +176,8 @@ impl<'a> NetworkActivityBuilder<'a> { } } +impl_builder_setters!(NetworkActivityBuilder); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/process.rs b/crates/openshell-ocsf/src/builders/process.rs index 8ede8012c..d66790eae 100644 --- a/crates/openshell-ocsf/src/builders/process.rs +++ b/crates/openshell-ocsf/src/builders/process.rs @@ -48,16 +48,6 @@ impl<'a> ProcessActivityBuilder<'a> { 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 @@ -87,11 +77,6 @@ impl<'a> ProcessActivityBuilder<'a> { 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 { @@ -107,14 +92,8 @@ impl<'a> ProcessActivityBuilder<'a> { 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()); + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::ProcessActivity(ProcessActivityEvent { base, @@ -128,6 +107,8 @@ impl<'a> ProcessActivityBuilder<'a> { } } +impl_builder_setters!(ProcessActivityBuilder); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-ocsf/src/builders/ssh.rs b/crates/openshell-ocsf/src/builders/ssh.rs index 6df01f3d1..0df704f31 100644 --- a/crates/openshell-ocsf/src/builders/ssh.rs +++ b/crates/openshell-ocsf/src/builders/ssh.rs @@ -64,16 +64,6 @@ impl<'a> SshActivityBuilder<'a> { 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 @@ -88,11 +78,6 @@ impl<'a> SshActivityBuilder<'a> { 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] @@ -122,14 +107,8 @@ impl<'a> SshActivityBuilder<'a> { 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()); + self.ctx + .apply_common_fields(&mut base, self.status, self.message); OcsfEvent::SshActivity(SshActivityEvent { base, @@ -145,6 +124,8 @@ impl<'a> SshActivityBuilder<'a> { } } +impl_builder_setters!(SshActivityBuilder); + #[cfg(test)] mod tests { use super::*;