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;
|
pub mod host;
|
||||||
use ed25519_dalek::{PUBLIC_KEY_LENGTH, SigningKey};
|
pub mod manifest;
|
||||||
use glob::Pattern;
|
pub mod trace;
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use crate::{
|
||||||
|
host::{HostState, init_tracing},
|
||||||
|
manifest::load_manifest,
|
||||||
|
trace::{TraceEvent, load_trace},
|
||||||
|
};
|
||||||
use claims::{assert_ok, assert_some};
|
use claims::{assert_ok, assert_some};
|
||||||
|
use ed25519_dalek::SigningKey;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
@ -228,9 +71,9 @@ mod tests {
|
|||||||
let tmp_dir = tempdir().expect("Temp dir failed");
|
let tmp_dir = tempdir().expect("Temp dir failed");
|
||||||
let tmp_path = tmp_dir.as_ref().join("trace.json");
|
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);
|
assert_eq!(loaded_trace.len(), 2);
|
||||||
|
|
||||||
let loaded_trace1 = assert_some!(loaded_trace.first());
|
let loaded_trace1 = assert_some!(loaded_trace.first());
|
||||||
@ -252,13 +95,13 @@ mod tests {
|
|||||||
|
|
||||||
let mut host1 = HostState::new(manifest.clone(), fixed_seed, keypair1);
|
let mut host1 = HostState::new(manifest.clone(), fixed_seed, keypair1);
|
||||||
assert!(!host1.execute_plugin("./allowed.txt"));
|
assert!(!host1.execute_plugin("./allowed.txt"));
|
||||||
let trace1 = host1.finalize_trace();
|
let trace1 = host1.get_trace_json();
|
||||||
|
|
||||||
let mut csprng2 = OsRng;
|
let mut csprng2 = OsRng;
|
||||||
let keypair2 = SigningKey::generate(&mut csprng2);
|
let keypair2 = SigningKey::generate(&mut csprng2);
|
||||||
let mut host2 = HostState::new(manifest, fixed_seed, keypair2);
|
let mut host2 = HostState::new(manifest, fixed_seed, keypair2);
|
||||||
assert!(!host2.execute_plugin("./allowed.txt"));
|
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 parsed1 = assert_ok!(serde_json::from_str::<Vec<TraceEvent>>(&trace1));
|
||||||
let parsed2 = assert_ok!(serde_json::from_str::<Vec<TraceEvent>>(&trace2));
|
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