mirror of
https://github.com/kristoferssolo/captra.git
synced 2025-12-20 11:04:39 +00:00
refactor: separate into modules
This commit is contained in:
parent
1061d14c1a
commit
8f1bbc88cf
99
src/host.rs
Normal file
99
src/host.rs
Normal 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();
|
||||
}
|
||||
183
src/lib.rs
183
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<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
43
src/manifest.rs
Normal 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
69
src/trace.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user