refactor: separate into modules

This commit is contained in:
Kristofers Solo 2025-09-28 15:48:05 +03:00
parent 1061d14c1a
commit 8f1bbc88cf
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
4 changed files with 224 additions and 170 deletions

99
src/host.rs Normal file
View File

@ -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<TraceEvent>,
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&self, path: P) -> Result<()> {
save_trace(&self.trace, path)
}
}
pub fn init_tracing() {
tracing_subscriber::fmt().with_max_level(Level::INFO).init();
}

View File

@ -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<Vec<String>>, // Glob patter for read
pub write: Option<Vec<String>>, // 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<FsCapability>,
}
#[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<TraceEvent>,
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<P: AsRef<Path>>(&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<P: AsRef<Path>>(&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<TraceEvent>.
///
/// # Errors
///
/// If file read fails (e.g., I/O error) or JSON parsing fails.
pub fn load_trace<P: AsRef<Path>>(path: P) -> Result<Vec<TraceEvent>> {
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<CapabilityManifest> {
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::<Vec<TraceEvent>>(&trace1));
let parsed2 = assert_ok!(serde_json::from_str::<Vec<TraceEvent>>(&trace2));

43
src/manifest.rs Normal file
View File

@ -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<Vec<String>>, // Glob patter for read
pub write: Option<Vec<String>>, // 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<FsCapability>,
}
#[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<CapabilityManifest> {
let json_str = read_to_string(path)?;
let manifest = serde_json::from_str(&json_str)?;
Ok(manifest)
}

69
src/trace.rs Normal file
View File

@ -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<P: AsRef<Path>>(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<TraceEvent>.
///
/// # Errors
///
/// If file read fails (e.g., I/O error) or JSON parsing fails.
pub fn load_trace<P: AsRef<Path>>(path: P) -> Result<Vec<TraceEvent>> {
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,
);
}