Skip to main content

palisade_config/
validation.rs

1//! Configuration and policy diffing for change tracking.
2
3use crate::{Config, PolicyConfig};
4use crate::timing::{enforce_operation_min_timing, TimingOperation};
5use std::collections::HashSet;
6use std::path::PathBuf;
7use std::time::Instant;
8
9/// Validation strictness level.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ValidationMode {
12    /// Standard validation (format checks, no filesystem access)
13    Standard,
14
15    /// Strict validation (paths must exist, permissions verified)
16    Strict,
17}
18
19/// Configuration change detected during diff.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ConfigChange {
22    /// Root tag changed (shows hash, not secret)
23    RootTagChanged { old_hash: String, new_hash: String },
24
25    /// Decoy paths changed
26    PathsChanged {
27        added: Vec<PathBuf>,
28        removed: Vec<PathBuf>,
29    },
30
31    /// Capability settings changed
32    CapabilitiesChanged {
33        field: String,
34        old: String,
35        new: String,
36    },
37}
38
39/// Policy change detected during diff.
40#[derive(Debug, Clone, PartialEq)]
41pub enum PolicyChange {
42    /// Threshold value changed
43    ThresholdChanged {
44        field: String,
45        old: f64,
46        new: f64,
47    },
48
49    /// Response rules changed
50    ResponseRulesChanged {
51        old_count: usize,
52        new_count: usize,
53    },
54
55    /// Suspicious process patterns changed
56    SuspiciousProcessesChanged {
57        added: Vec<String>,
58        removed: Vec<String>,
59    },
60}
61
62impl Config {
63    /// Diff configuration against another configuration.
64    #[must_use]
65    pub fn diff(&self, other: &Config) -> Vec<ConfigChange> {
66        let started = Instant::now();
67        let mut changes = Vec::new();
68
69        // Compare root tags via hash (secure, no exposure)
70        if !self.deception.root_tag.hash_eq_ct(&other.deception.root_tag) {
71            changes.push(ConfigChange::RootTagChanged {
72                old_hash: hex::encode(&self.deception.root_tag.hash()[..8]),
73                new_hash: hex::encode(&other.deception.root_tag.hash()[..8]),
74            });
75        }
76
77        // Compare paths - use references to avoid cloning
78        let old_paths: HashSet<&PathBuf> = self.deception.decoy_paths.iter().collect();
79        let new_paths: HashSet<&PathBuf> = other.deception.decoy_paths.iter().collect();
80
81        let added: Vec<PathBuf> = new_paths
82            .difference(&old_paths)
83            .map(|&p| p.clone())  // Only clone when building final diff result
84            .collect();
85        let removed: Vec<PathBuf> = old_paths
86            .difference(&new_paths)
87            .map(|&p| p.clone())  // Only clone when building final diff result
88            .collect();
89
90        if !added.is_empty() || !removed.is_empty() {
91            changes.push(ConfigChange::PathsChanged { added, removed });
92        }
93
94        // Compare syscall monitoring capability
95        if self.telemetry.enable_syscall_monitor != other.telemetry.enable_syscall_monitor {
96            changes.push(ConfigChange::CapabilitiesChanged {
97                field: "enable_syscall_monitor".to_string(),
98                old: self.telemetry.enable_syscall_monitor.to_string(),
99                new: other.telemetry.enable_syscall_monitor.to_string(),
100            });
101        }
102
103        enforce_operation_min_timing(started, TimingOperation::ConfigDiff);
104        changes
105    }
106}
107
108impl PolicyConfig {
109    /// Diff policy against another policy.
110    #[must_use]
111    pub fn diff(&self, other: &PolicyConfig) -> Vec<PolicyChange> {
112        let started = Instant::now();
113        let mut changes = Vec::new();
114
115        // Threshold changes
116        if (self.scoring.alert_threshold - other.scoring.alert_threshold).abs() > 0.01 {
117            changes.push(PolicyChange::ThresholdChanged {
118                field: "alert_threshold".to_string(),
119                old: self.scoring.alert_threshold,
120                new: other.scoring.alert_threshold,
121            });
122        }
123
124        // Response rules
125        if self.response.rules.len() != other.response.rules.len() {
126            changes.push(PolicyChange::ResponseRulesChanged {
127                old_count: self.response.rules.len(),
128                new_count: other.response.rules.len(),
129            });
130        }
131
132        // Suspicious processes - use references to avoid cloning
133        let old: HashSet<&String> = self.deception.suspicious_processes.iter().collect();
134        let new: HashSet<&String> = other.deception.suspicious_processes.iter().collect();
135
136        let added: Vec<String> = new
137            .difference(&old)
138            .map(|&s| s.clone())  // Only clone when building final diff result
139            .collect();
140        let removed: Vec<String> = old
141            .difference(&new)
142            .map(|&s| s.clone())  // Only clone when building final diff result
143            .collect();
144
145        if !added.is_empty() || !removed.is_empty() {
146            changes.push(PolicyChange::SuspiciousProcessesChanged { added, removed });
147        }
148
149        enforce_operation_min_timing(started, TimingOperation::PolicyDiff);
150        changes
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::tags::RootTag;
158
159    #[test]
160    fn test_config_diff_detects_root_tag_change() {
161        let config1 = Config::default();
162        let mut config2 = Config::default();
163        config2.deception.root_tag = RootTag::generate().expect("Failed to generate tag");
164
165        let changes = config1.diff(&config2);
166        assert!(!changes.is_empty());
167
168        if let Some(ConfigChange::RootTagChanged { old_hash, new_hash }) = changes.first() {
169            assert_eq!(old_hash.len(), 16);
170            assert_eq!(new_hash.len(), 16);
171            assert_ne!(old_hash, new_hash);
172        } else {
173            panic!("Expected RootTagChanged");
174        }
175    }
176
177    #[test]
178    fn test_policy_diff_detects_threshold_change() {
179        let mut policy1 = PolicyConfig::default();
180        let mut policy2 = PolicyConfig::default();
181
182        policy1.scoring.alert_threshold = 50.0;
183        policy2.scoring.alert_threshold = 75.0;
184
185        let changes = policy1.diff(&policy2);
186        assert!(!changes.is_empty());
187
188        if let Some(PolicyChange::ThresholdChanged { field, old, new }) = changes.first() {
189            assert_eq!(field, "alert_threshold");
190            assert_eq!(*old, 50.0);
191            assert_eq!(*new, 75.0);
192        } else {
193            panic!("Expected ThresholdChanged");
194        }
195    }
196}