diff --git a/Cargo.lock b/Cargo.lock index 69d50de..6626540 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -41,6 +50,8 @@ dependencies = [ "glob", "serde", "serde_json", + "tracing", + "tracing-subscriber", ] [[package]] @@ -128,6 +139,21 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.5" @@ -143,6 +169,15 @@ dependencies = [ "adler2", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys", +] + [[package]] name = "object" version = "0.36.7" @@ -188,6 +223,23 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -252,6 +304,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "syn" version = "2.0.106" @@ -279,9 +337,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -302,15 +372,46 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", "sharded-slab", + "smallvec", "thread_local", + "tracing", "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -325,6 +426,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index ac63e92..98f74e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ color-eyre = "0.6" glob = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } [dev-dependencies] claims = "0.8" diff --git a/src/lib.rs b/src/lib.rs index 263982e..b969630 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use color_eyre::eyre::Result; use glob::Pattern; use serde::{Deserialize, Serialize}; use std::{fs::read_to_string, path::Path}; +use tracing::{Level, info}; #[derive(Debug, Serialize, Deserialize)] pub struct FsCapability { @@ -29,21 +30,36 @@ pub struct CapabilityManifest { // TODO: add signature } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TraceEvent { + pub seq: u64, + pub event_type: String, + pub input: String, + pub outcome: bool, +} + +#[derive(Debug)] pub struct HostState { pub manifest: CapabilityManifest, + pub trace: Vec, } impl HostState { #[inline] #[must_use] pub const fn new(manifest: CapabilityManifest) -> Self { - Self { manifest } + Self { + manifest, + trace: Vec::new(), + } } /// 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>(&self, path: P) -> bool { + pub fn execute_plugin>(&mut self, path: P) -> bool { + let seq = u64::try_from(self.trace.len()).map_or_else(|_| 1, |len| len + 1); + let Some(fs_cap) = &self.manifest.capabilities.fs else { return false; }; @@ -54,11 +70,34 @@ impl HostState { let path_str = path.as_ref().to_string_lossy(); - read_patterns.iter().any(|pattern| { + let is_allowed = read_patterns.iter().any(|pattern| { Pattern::new(pattern) .map(|p| p.matches(&path_str)) .unwrap_or(false) - }) + }); + + info!( + seq = seq, + 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, + }); + + is_allowed + } + + #[inline] + #[must_use] + pub fn finalize_trace(&self) -> String { + serde_json::to_string_pretty(&self.trace).unwrap_or_else(|_| "[]".into()) } } @@ -73,10 +112,19 @@ pub fn load_manifest(path: &str) -> Result { Ok(manifest) } +pub fn init_tracing() { + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); +} + #[cfg(test)] mod tests { use super::*; - use claims::assert_some; + use claims::{assert_ok, assert_some}; + + #[test] + fn tracing_init() { + init_tracing(); + } #[test] fn manifest_loader() { @@ -94,13 +142,35 @@ mod tests { } #[test] - fn host_enforcement() { - let manifest = load_manifest("examples/manifest.json").expect("Load failed"); - let host = HostState::new(manifest); + fn host_enforcement_with_trace() { + init_tracing(); - // Allowed: matches ./workspace/* - assert!(host.execute_plugin("./workspace/config.toml")); - // Disallowed: outside pattern - assert!(!host.execute_plugin("/etc/passwd")) + let manifest = load_manifest("examples/manifest.json").expect("Load failed"); + let mut host = HostState::new(manifest); + + // allowed - expect a tracing event + let out1 = host.execute_plugin("./workspace/config.toml"); + assert!(out1); + + // denied - event + entry + let out2 = host.execute_plugin("/etc/passwd"); + assert!(!out2); + + assert_eq!(host.trace.len(), 2); + + let trace1 = assert_some!(host.trace.first()); + assert_eq!(trace1.seq, 1); + assert_eq!(trace1.event_type, "cap.call"); + assert_eq!(trace1.input, "./workspace/config.toml"); + assert!(trace1.outcome); + + let trace2 = assert_some!(host.trace.get(1)); + assert_eq!(trace2.seq, 2); + assert_eq!(trace2.input, "/etc/passwd"); + assert!(!trace2.outcome); + + let json = host.finalize_trace(); + let parsed = assert_ok!(serde_json::from_str::>(&json)); + assert_eq!(parsed.len(), 2); } }