feat: add seed

This commit is contained in:
Kristofers Solo 2025-09-25 13:57:44 +03:00
parent fab9efeac3
commit 3c9fdc691a
Signed by: kristoferssolo
GPG Key ID: 74FF8144483D82C8
3 changed files with 109 additions and 11 deletions

59
Cargo.lock generated
View File

@ -54,6 +54,7 @@ dependencies = [
"claims", "claims",
"color-eyre", "color-eyre",
"glob", "glob",
"rand",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
@ -246,6 +247,15 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.101" version = "1.0.101"
@ -270,6 +280,35 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.10" version = "0.4.10"
@ -595,3 +634,23 @@ name = "wit-bindgen"
version = "0.46.0" version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -12,6 +12,7 @@ categories = ["cryptography::randomness", "encoding"]
[dependencies] [dependencies]
color-eyre = "0.6" color-eyre = "0.6"
glob = "0.3" glob = "0.3"
rand = "0.9"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tracing = "0.1" tracing = "0.1"

View File

@ -1,5 +1,6 @@
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use glob::Pattern; use glob::Pattern;
use rand::{Rng, SeedableRng, rngs::StdRng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fs::{self, read_to_string}, fs::{self, read_to_string},
@ -7,7 +8,10 @@ use std::{
}; };
use tracing::{Level, info}; use tracing::{Level, info};
#[derive(Debug, Serialize, Deserialize)] /// 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 struct FsCapability {
pub read: Option<Vec<String>>, // Glob patter for read pub read: Option<Vec<String>>, // Glob patter for read
pub write: Option<Vec<String>>, // Stub for now pub write: Option<Vec<String>>, // Stub for now
@ -19,12 +23,12 @@ pub enum Capability {
// TODO: add Net, Cpu, etc // TODO: add Net, Cpu, etc
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Capabilities { pub struct Capabilities {
pub fs: Option<FsCapability>, pub fs: Option<FsCapability>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityManifest { pub struct CapabilityManifest {
pub plugin: String, pub plugin: String,
pub version: String, pub version: String,
@ -39,21 +43,24 @@ pub struct TraceEvent {
pub event_type: String, pub event_type: String,
pub input: String, pub input: String,
pub outcome: bool, pub outcome: bool,
pub ts_seed: u64,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct HostState { pub struct HostState {
pub manifest: CapabilityManifest, pub manifest: CapabilityManifest,
pub trace: Vec<TraceEvent>, pub trace: Vec<TraceEvent>,
pub seed: u64,
} }
impl HostState { impl HostState {
#[inline] #[inline]
#[must_use] #[must_use]
pub const fn new(manifest: CapabilityManifest) -> Self { pub const fn new(manifest: CapabilityManifest, seed: u64) -> Self {
Self { Self {
manifest, manifest,
trace: Vec::new(), trace: Vec::new(),
seed,
} }
} }
@ -61,8 +68,6 @@ impl HostState {
/// Returns `true` if allowed, `false` if denied. /// Returns `true` if allowed, `false` if denied.
#[must_use] #[must_use]
pub fn execute_plugin<P: AsRef<Path>>(&mut self, path: P) -> bool { pub fn execute_plugin<P: AsRef<Path>>(&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 { let Some(fs_cap) = &self.manifest.capabilities.fs else {
return false; return false;
}; };
@ -71,8 +76,12 @@ impl HostState {
return false; 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 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.random();
let is_allowed = read_patterns.iter().any(|pattern| { let is_allowed = read_patterns.iter().any(|pattern| {
Pattern::new(pattern) Pattern::new(pattern)
.map(|p| p.matches(&path_str)) .map(|p| p.matches(&path_str))
@ -81,6 +90,7 @@ impl HostState {
info!( info!(
seq = seq, seq = seq,
ts_seed = ts_seed,
event_type = "cap.call", event_type = "cap.call",
input = %path_str, input = %path_str,
outcome = is_allowed, outcome = is_allowed,
@ -92,6 +102,7 @@ impl HostState {
event_type: "cap.call".into(), event_type: "cap.call".into(),
input: path_str.into(), input: path_str.into(),
outcome: is_allowed, outcome: is_allowed,
ts_seed,
}); });
is_allowed is_allowed
@ -152,7 +163,7 @@ mod tests {
#[test] #[test]
fn manifest_loader() { fn manifest_loader() {
let manifest = load_manifest("examples/manifest.json").expect("Load failed"); let manifest = assert_ok!(load_manifest("examples/manifest.json"), "Load failed");
assert_eq!(manifest.plugin, "formatter-v1"); assert_eq!(manifest.plugin, "formatter-v1");
assert_eq!(manifest.version, "0.1"); assert_eq!(manifest.version, "0.1");
assert_eq!(manifest.issued_by, "dev-team"); assert_eq!(manifest.issued_by, "dev-team");
@ -169,8 +180,9 @@ mod tests {
fn host_enforcement_with_trace() { fn host_enforcement_with_trace() {
init_tracing(); init_tracing();
let manifest = load_manifest("examples/manifest.json").expect("Load failed"); let manifest = assert_ok!(load_manifest("examples/manifest.json"), "Load failed");
let mut host = HostState::new(manifest); let fixed_seed = 12345;
let mut host = HostState::new(manifest, fixed_seed);
// allowed - expect a tracing event // allowed - expect a tracing event
let out1 = host.execute_plugin("./workspace/config.toml"); let out1 = host.execute_plugin("./workspace/config.toml");
@ -186,18 +198,22 @@ mod tests {
assert_eq!(trace1.event_type, "cap.call"); assert_eq!(trace1.event_type, "cap.call");
assert_eq!(trace1.input, "./workspace/config.toml"); assert_eq!(trace1.input, "./workspace/config.toml");
assert!(trace1.outcome); assert!(trace1.outcome);
assert_eq!(trace1.ts_seed, 8_166_419_713_379_829_776);
assert_ne!(trace1.ts_seed, 0);
let trace2 = assert_some!(host.trace.get(1)); let trace2 = assert_some!(host.trace.get(1));
assert_eq!(trace2.seq, 2); assert_eq!(trace2.seq, 2);
assert_eq!(trace2.input, "/etc/passwd"); assert_eq!(trace2.input, "/etc/passwd");
assert!(!trace2.outcome); assert!(!trace2.outcome);
assert_eq!(trace2.ts_seed, 10_553_447_931_939_622_718);
assert_ne!(trace1.ts_seed, trace2.ts_seed);
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)); assert_ok!(host.save_trace(&tmp_path), "Save failed");
let loaded_trace = assert_ok!(HostState::load_trace(tmp_path)); let loaded_trace = assert_ok!(HostState::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());
@ -206,4 +222,26 @@ mod tests {
let loaded_trace2 = assert_some!(loaded_trace.get(1)); let loaded_trace2 = assert_some!(loaded_trace.get(1));
assert_eq!(trace2, loaded_trace2); assert_eq!(trace2, loaded_trace2);
} }
#[test]
fn trace_reproducibility() {
init_tracing();
let manifest = assert_ok!(load_manifest("examples/manifest.json"), "Load failed");
let fixed_seed = 12345;
let mut host1 = HostState::new(manifest.clone(), fixed_seed);
assert!(!host1.execute_plugin("./allowed.txt"));
let trace1 = host1.finalize_trace();
let mut host2 = HostState::new(manifest, fixed_seed);
assert!(!host2.execute_plugin("./allowed.txt"));
let trace2 = host2.finalize_trace();
let parsed1 = assert_ok!(serde_json::from_str::<Vec<TraceEvent>>(&trace1));
let parsed2 = assert_ok!(serde_json::from_str::<Vec<TraceEvent>>(&trace2));
assert_eq!(parsed1, parsed2);
assert_eq!(parsed1[0].ts_seed, parsed2[0].ts_seed);
}
} }