use crate::models::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoldenTraceConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_trace_dir")]
pub trace_dir: String,
#[serde(default = "default_capture_categories")]
pub capture: Vec<String>,
#[serde(default = "default_ignore_paths")]
pub ignore_paths: Vec<String>,
}
fn default_trace_dir() -> String {
".golden-traces".to_string()
}
fn default_capture_categories() -> Vec<String> {
vec![
"file".to_string(),
"network".to_string(),
"process".to_string(),
"permission".to_string(),
]
}
fn default_ignore_paths() -> Vec<String> {
vec![
"/proc/*".to_string(),
"/sys/*".to_string(),
"/dev/null".to_string(),
"/tmp/bashrs-*".to_string(),
]
}
impl Default for GoldenTraceConfig {
fn default() -> Self {
Self {
enabled: false,
trace_dir: default_trace_dir(),
capture: default_capture_categories(),
ignore_paths: default_ignore_paths(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TraceEventType {
File {
operation: String,
path: String,
flags: Option<String>,
},
Network {
operation: String,
address: Option<String>,
port: Option<u16>,
},
Process {
operation: String,
command: Option<String>,
args: Option<Vec<String>>,
},
Permission {
operation: String,
path: String,
mode: Option<u32>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceEvent {
pub sequence: u64,
pub timestamp_ns: u64,
pub event_type: TraceEventType,
pub step_id: Option<String>,
pub result: TraceResult,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TraceResult {
Success,
Error(i32),
Unknown,
}
impl TraceEvent {
pub fn summary(&self) -> String {
match &self.event_type {
TraceEventType::File {
operation, path, ..
} => {
format!("{}(\"{}\")", operation, path)
}
TraceEventType::Network {
operation,
address,
port,
} => {
if let (Some(addr), Some(p)) = (address, port) {
format!("{}({}:{})", operation, addr, p)
} else {
operation.clone()
}
}
TraceEventType::Process {
operation, command, ..
} => {
if let Some(cmd) = command {
format!("{}(\"{}\")", operation, cmd)
} else {
operation.clone()
}
}
TraceEventType::Permission {
operation,
path,
mode,
} => {
if let Some(m) = mode {
format!("{}(\"{}\", {:o})", operation, path, m)
} else {
format!("{}(\"{}\")", operation, path)
}
}
}
}
pub fn category(&self) -> &'static str {
match &self.event_type {
TraceEventType::File { .. } => "file",
TraceEventType::Network { .. } => "network",
TraceEventType::Process { .. } => "process",
TraceEventType::Permission { .. } => "permission",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoldenTrace {
pub name: String,
pub captured_at: String,
pub installer_version: String,
pub events: Vec<TraceEvent>,
pub result_hash: String,
pub steps_executed: usize,
pub duration_ms: u64,
}
impl GoldenTrace {
pub fn save(&self, path: &Path) -> Result<()> {
let content = serde_json::to_string_pretty(self)
.map_err(|e| Error::Validation(format!("Failed to serialize golden trace: {}", e)))?;
std::fs::write(path, content).map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!("Failed to write golden trace: {}", e),
))
})?;
Ok(())
}
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!("Failed to read golden trace: {}", e),
))
})?;
serde_json::from_str(&content)
.map_err(|e| Error::Validation(format!("Failed to parse golden trace: {}", e)))
}
}
#[derive(Debug, Clone, Default)]
pub struct TraceComparison {
pub added: Vec<TraceEvent>,
pub removed: Vec<TraceEvent>,
pub changed: Vec<(TraceEvent, TraceEvent)>,
pub metadata: ComparisonMetadata,
}
#[derive(Debug, Clone, Default)]
pub struct ComparisonMetadata {
pub golden_name: String,
pub golden_captured_at: String,
pub current_captured_at: String,
pub golden_event_count: usize,
pub current_event_count: usize,
}
impl TraceComparison {
pub fn is_equivalent(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.changed.is_empty()
}
pub fn to_report(&self) -> String {
let mut report = String::new();
report.push_str(&format!(
"Golden Trace Comparison: {}\n",
self.metadata.golden_name
));
report.push_str(&format!(
"Golden captured: {}, Current: {}\n",
self.metadata.golden_captured_at, self.metadata.current_captured_at
));
report.push_str(&format!(
"Events: {} (golden) vs {} (current)\n\n",
self.metadata.golden_event_count, self.metadata.current_event_count
));
if self.is_equivalent() {
report.push_str("✅ Traces are EQUIVALENT - no regression detected\n");
return report;
}
if !self.added.is_empty() {
report.push_str("=== New events (potential security concern) ===\n");
for event in &self.added {
report.push_str(&format!(
"+ [{}] {} ({:?})\n",
event.category(),
event.summary(),
event.result
));
}
report.push('\n');
}
if !self.removed.is_empty() {
report.push_str("=== Missing events (potential regression) ===\n");
for event in &self.removed {
report.push_str(&format!(
"- [{}] {} ({:?})\n",
event.category(),
event.summary(),
event.result
));
}
report.push('\n');
}
if !self.changed.is_empty() {
report.push_str("=== Changed events ===\n");
for (old, new) in &self.changed {
report.push_str(&format!(
"~ [{}] {} -> {}\n",
old.category(),
old.summary(),
new.summary()
));
}
}
report
}
pub fn compare(golden: &GoldenTrace, current: &GoldenTrace) -> Self {
let mut comparison = TraceComparison {
metadata: ComparisonMetadata {
golden_name: golden.name.clone(),
golden_captured_at: golden.captured_at.clone(),
current_captured_at: current.captured_at.clone(),
golden_event_count: golden.events.len(),
current_event_count: current.events.len(),
},
..Default::default()
};
let golden_sigs: HashSet<String> = golden.events.iter().map(|e| e.summary()).collect();
let current_sigs: HashSet<String> = current.events.iter().map(|e| e.summary()).collect();
for event in ¤t.events {
if !golden_sigs.contains(&event.summary()) {
comparison.added.push(event.clone());
}
}
for event in &golden.events {
if !current_sigs.contains(&event.summary()) {
comparison.removed.push(event.clone());
}
}
comparison
}
}
include!("golden_trace_goldentracemanager.rs");