From 8f1bbc88cf4e2c8fe14b85da39ca0e21947c5924 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Sun, 28 Sep 2025 15:48:05 +0300 Subject: [PATCH] refactor: separate into modules --- src/host.rs | 99 ++++++++++++++++++++++++++ src/lib.rs | 183 ++++-------------------------------------------- src/manifest.rs | 43 ++++++++++++ src/trace.rs | 69 ++++++++++++++++++ 4 files changed, 224 insertions(+), 170 deletions(-) create mode 100644 src/host.rs create mode 100644 src/manifest.rs create mode 100644 src/trace.rs diff --git a/src/host.rs b/src/host.rs new file mode 100644 index 0000000..ef9cfce --- /dev/null +++ b/src/host.rs @@ -0,0 +1,99 @@ +use color_eyre::Result; +use ed25519_dalek::{PUBLIC_KEY_LENGTH, SigningKey}; +use glob::Pattern; +use rand::{Rng, SeedableRng, rngs::StdRng}; +use std::path::Path; +use tracing::Level; + +use crate::{ + manifest::{CapabilityManifest, PRIME_MULTIPLIER}, + trace::{TraceEvent, finalize_trace, log_trace_event, save_trace}, +}; + +#[derive(Debug)] +pub struct HostState { + pub manifest: CapabilityManifest, + pub trace: Vec, + pub seed: u64, + pub keypair: SigningKey, + pub pubkey: [u8; PUBLIC_KEY_LENGTH], +} + +impl HostState { + #[inline] + #[must_use] + pub fn new(manifest: CapabilityManifest, seed: u64, keypair: SigningKey) -> Self { + let pubkey = keypair.verifying_key().to_bytes(); + Self { + manifest, + trace: Vec::new(), + seed, + keypair, + pubkey, + } + } + + /// Simulate "plugin execution": check if path is allowed via FS read cap. + /// Returns `true` if allowed, `false` if denied. + #[must_use] + pub fn execute_plugin>(&mut self, path: P) -> bool { + let Some(fs_cap) = &self.manifest.capabilities.fs else { + return false; + }; + + let Some(read_patterns) = &fs_cap.read else { + return false; + }; + + let seq = u64::try_from(self.trace.len()).map_or(1, |len| len + 1); + let path_str = path.as_ref().to_string_lossy(); + + let mut rng = StdRng::seed_from_u64(self.seed.wrapping_mul(PRIME_MULTIPLIER + seq)); + let ts_seed = rng.r#gen(); + + let is_allowed = read_patterns.iter().any(|pattern| { + Pattern::new(pattern) + .map(|p| p.matches(&path_str)) + .unwrap_or(false) + }); + + log_trace_event( + seq, + "cap.call", + &path_str, + is_allowed, + ts_seed, + &self.manifest.plugin, + ); + + self.trace.push(TraceEvent { + seq, + event_type: "cap.call".into(), + input: path_str.into(), + outcome: is_allowed, + ts_seed, + }); + + is_allowed + } + + /// Serialize trace to pretty JSON string + #[inline] + #[must_use] + pub fn get_trace_json(&self) -> String { + finalize_trace(&self.trace) + } + + /// Save the current trace to a file as pretty JSON. + /// + /// # Errors + /// + /// If file write fails (e.g., I/O error) or JSON serialization fails. + pub fn save_current_trace>(&self, path: P) -> Result<()> { + save_trace(&self.trace, path) + } +} + +pub fn init_tracing() { + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); +} diff --git a/src/lib.rs b/src/lib.rs index 7501cfb..f359d2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,173 +1,16 @@ -use color_eyre::eyre::Result; -use ed25519_dalek::{PUBLIC_KEY_LENGTH, SigningKey}; -use glob::Pattern; -use rand::{Rng, SeedableRng, rngs::StdRng}; -use serde::{Deserialize, Serialize}; -use std::{ - fs::{self, read_to_string}, - path::Path, -}; -use tracing::{Level, info}; - -/// Prime for seq hashing to derive per-event RNG state -const PRIME_MULTIPLIER: u64 = 314_159; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FsCapability { - pub read: Option>, // Glob patter for read - pub write: Option>, // Stub for now -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum Capability { - Fs(FsCapability), - // TODO: add Net, Cpu, etc -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Capabilities { - pub fs: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CapabilityManifest { - pub plugin: String, - pub version: String, - pub capabilities: Capabilities, - pub issued_by: String, - // TODO: add signature -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TraceEvent { - pub seq: u64, - pub event_type: String, - pub input: String, - pub outcome: bool, - pub ts_seed: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SignedTrace { - pub run_id: String, - pub manifest_hash: String, - pub trace_json: String, - pub signature: String, -} - -#[derive(Debug)] -pub struct HostState { - pub manifest: CapabilityManifest, - pub trace: Vec, - pub seed: u64, - pub keypair: SigningKey, - pub pubkey: [u8; PUBLIC_KEY_LENGTH], -} - -impl HostState { - #[inline] - #[must_use] - pub fn new(manifest: CapabilityManifest, seed: u64, keypair: SigningKey) -> Self { - let pubkey = keypair.verifying_key().to_bytes(); - Self { - manifest, - trace: Vec::new(), - seed, - keypair, - pubkey, - } - } - - /// Simulate "plugin execution": check if path is allowed via FS read cap. - /// Returns `true` if allowed, `false` if denied. - #[must_use] - pub fn execute_plugin>(&mut self, path: P) -> bool { - let Some(fs_cap) = &self.manifest.capabilities.fs else { - return false; - }; - - let Some(read_patterns) = &fs_cap.read else { - return false; - }; - - let seq = u64::try_from(self.trace.len()).map_or(1, |len| len + 1); - let path_str = path.as_ref().to_string_lossy(); - - let mut rng = StdRng::seed_from_u64(self.seed.wrapping_mul(PRIME_MULTIPLIER + seq)); - let ts_seed = rng.r#gen(); - - let is_allowed = read_patterns.iter().any(|pattern| { - Pattern::new(pattern) - .map(|p| p.matches(&path_str)) - .unwrap_or(false) - }); - - info!( - seq = seq, - ts_seed = ts_seed, - event_type = "cap.call", - input = %path_str, - outcome = is_allowed, - plugin = %self.manifest.plugin - ); - - self.trace.push(TraceEvent { - seq, - event_type: "cap.call".into(), - input: path_str.into(), - outcome: is_allowed, - ts_seed, - }); - - is_allowed - } - - #[inline] - #[must_use] - pub fn finalize_trace(&self) -> String { - serde_json::to_string_pretty(&self.trace).unwrap_or_else(|_| "[]".into()) - } - - /// Save the current trace to a file as pretty JSON. - /// # Errors - /// If file write fails (e.g., I/O error) or JSON serialization fails. - pub fn save_trace>(&self, path: P) -> Result<()> { - let json_str = serde_json::to_string_pretty(&self.trace)?; - fs::write(path, json_str)?; - Ok(()) - } - - /// Load a trace from a JSON file to Vec. - /// - /// # Errors - /// - /// If file read fails (e.g., I/O error) or JSON parsing fails. - pub fn load_trace>(path: P) -> Result> { - let json_str = fs::read_to_string(path)?; - let trace = serde_json::from_str(&json_str)?; - Ok(trace) - } -} - -/// Loads a capability manifest from a JSON file. -/// -/// # Errors -/// -/// - bubbles up `std::fs::read_to_string` and `serde_json::from_str` errors; -pub fn load_manifest(path: &str) -> Result { - let json_str = read_to_string(path)?; - let manifest = serde_json::from_str(&json_str)?; - Ok(manifest) -} - -pub fn init_tracing() { - tracing_subscriber::fmt().with_max_level(Level::INFO).init(); -} +pub mod host; +pub mod manifest; +pub mod trace; #[cfg(test)] mod tests { - use super::*; + use crate::{ + host::{HostState, init_tracing}, + manifest::load_manifest, + trace::{TraceEvent, load_trace}, + }; use claims::{assert_ok, assert_some}; + use ed25519_dalek::SigningKey; use rand::rngs::OsRng; use tempfile::tempdir; @@ -228,9 +71,9 @@ mod tests { let tmp_dir = tempdir().expect("Temp dir failed"); let tmp_path = tmp_dir.as_ref().join("trace.json"); - assert_ok!(host.save_trace(&tmp_path), "Save failed"); + assert_ok!(host.save_current_trace(&tmp_path), "Save failed"); - let loaded_trace = assert_ok!(HostState::load_trace(tmp_path), "Load failed"); + let loaded_trace = assert_ok!(load_trace(tmp_path), "Load failed"); assert_eq!(loaded_trace.len(), 2); let loaded_trace1 = assert_some!(loaded_trace.first()); @@ -252,13 +95,13 @@ mod tests { let mut host1 = HostState::new(manifest.clone(), fixed_seed, keypair1); assert!(!host1.execute_plugin("./allowed.txt")); - let trace1 = host1.finalize_trace(); + let trace1 = host1.get_trace_json(); let mut csprng2 = OsRng; let keypair2 = SigningKey::generate(&mut csprng2); let mut host2 = HostState::new(manifest, fixed_seed, keypair2); assert!(!host2.execute_plugin("./allowed.txt")); - let trace2 = host2.finalize_trace(); + let trace2 = host2.get_trace_json(); let parsed1 = assert_ok!(serde_json::from_str::>(&trace1)); let parsed2 = assert_ok!(serde_json::from_str::>(&trace2)); diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 0000000..0144184 --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,43 @@ +use color_eyre::eyre::Result; +use serde::{Deserialize, Serialize}; +use std::fs::read_to_string; + +/// Prime for seq hashing to derive per-event RNG state +pub const PRIME_MULTIPLIER: u64 = 314_159; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FsCapability { + pub read: Option>, // Glob patter for read + pub write: Option>, // Stub for now +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Capability { + Fs(FsCapability), + // TODO: add Net, Cpu, etc +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Capabilities { + pub fs: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CapabilityManifest { + pub plugin: String, + pub version: String, + pub capabilities: Capabilities, + pub issued_by: String, + // TODO: add signature +} + +/// Loads a capability manifest from a JSON file. +/// +/// # Errors +/// +/// - bubbles up `std::fs::read_to_string` and `serde_json::from_str` errors; +pub fn load_manifest(path: &str) -> Result { + let json_str = read_to_string(path)?; + let manifest = serde_json::from_str(&json_str)?; + Ok(manifest) +} diff --git a/src/trace.rs b/src/trace.rs new file mode 100644 index 0000000..d31eb95 --- /dev/null +++ b/src/trace.rs @@ -0,0 +1,69 @@ +use color_eyre::eyre::Result; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path}; +use tracing::info; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TraceEvent { + pub seq: u64, + pub event_type: String, + pub input: String, + pub outcome: bool, + pub ts_seed: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedTrace { + pub run_id: String, + pub manifest_hash: String, + pub trace_json: String, + pub signature: String, +} + +/// Save the current trace to a file as pretty JSON. +/// +/// # Errors +/// +/// If file write fails (e.g., I/O error) or JSON serialization fails. +pub fn save_trace>(trace: &[TraceEvent], path: P) -> Result<()> { + let json_str = serde_json::to_string_pretty(trace)?; + fs::write(path, json_str)?; + Ok(()) +} + +/// Load a trace from a JSON file to Vec. +/// +/// # Errors +/// +/// If file read fails (e.g., I/O error) or JSON parsing fails. +pub fn load_trace>(path: P) -> Result> { + let json_str = fs::read_to_string(path)?; + let trace = serde_json::from_str(&json_str)?; + Ok(trace) +} + +/// Serialize trace to pretty JSON string (fallback to "[]"). +#[inline] +#[must_use] +pub fn finalize_trace(trace: &[TraceEvent]) -> String { + serde_json::to_string_pretty(trace).unwrap_or_else(|_| "[]".into()) +} + +/// Log a trace event +pub fn log_trace_event( + seq: u64, + event_type: &str, + input: &str, + outcome: bool, + ts_seed: u64, + plugin: &str, +) { + info!( + seq = seq, + ts_seed = ts_seed, + event_type = event_type, + input = %input, + outcome = outcome, + plugin = plugin, + ); +}