palisade_config/
validation.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ValidationMode {
12 Standard,
14
15 Strict,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ConfigChange {
22 RootTagChanged { old_hash: String, new_hash: String },
24
25 PathsChanged {
27 added: Vec<PathBuf>,
28 removed: Vec<PathBuf>,
29 },
30
31 CapabilitiesChanged {
33 field: String,
34 old: String,
35 new: String,
36 },
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum PolicyChange {
42 ThresholdChanged {
44 field: String,
45 old: f64,
46 new: f64,
47 },
48
49 ResponseRulesChanged {
51 old_count: usize,
52 new_count: usize,
53 },
54
55 SuspiciousProcessesChanged {
57 added: Vec<String>,
58 removed: Vec<String>,
59 },
60}
61
62impl Config {
63 #[must_use]
65 pub fn diff(&self, other: &Config) -> Vec<ConfigChange> {
66 let started = Instant::now();
67 let mut changes = Vec::new();
68
69 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 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()) .collect();
85 let removed: Vec<PathBuf> = old_paths
86 .difference(&new_paths)
87 .map(|&p| p.clone()) .collect();
89
90 if !added.is_empty() || !removed.is_empty() {
91 changes.push(ConfigChange::PathsChanged { added, removed });
92 }
93
94 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 #[must_use]
111 pub fn diff(&self, other: &PolicyConfig) -> Vec<PolicyChange> {
112 let started = Instant::now();
113 let mut changes = Vec::new();
114
115 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 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 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()) .collect();
140 let removed: Vec<String> = old
141 .difference(&new)
142 .map(|&s| s.clone()) .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}