1use crate::timing::{TimingOperation, enforce_operation_min_timing};
4use crate::{AgentError, Config, PolicyConfig, Result};
5use core::fmt::Write as _;
6use heapless::{String as HString, Vec as HVec};
7use std::path::Path;
8use std::time::Instant;
9
10#[derive(Debug, PartialEq, Eq)]
12pub enum ValidationMode {
13 Standard,
15
16 Strict,
18}
19
20impl Default for ValidationMode {
21 fn default() -> Self {
22 Self::Standard
23 }
24}
25
26const CFG_VALIDATION_FAILED: u16 = 101;
27const MAX_CONFIG_DIFF_CHANGES: usize = 32;
28const MAX_POLICY_DIFF_CHANGES: usize = 32;
29const HASH_PREFIX_HEX_LEN: usize = 16;
30
31pub type ConfigDiff<'a> = HVec<ConfigChange<'a>, MAX_CONFIG_DIFF_CHANGES>;
33
34pub type PolicyDiff<'a> = HVec<PolicyChange<'a>, MAX_POLICY_DIFF_CHANGES>;
36
37#[derive(Debug, PartialEq, Eq)]
39pub enum ConfigChange<'a> {
40 RootTagChanged {
42 old_hash: HString<HASH_PREFIX_HEX_LEN>,
44 new_hash: HString<HASH_PREFIX_HEX_LEN>,
46 },
47
48 PathAdded {
50 path: &'a Path,
52 },
53
54 PathRemoved {
56 path: &'a Path,
58 },
59
60 CapabilitiesChanged {
62 field: &'static str,
64 old: bool,
66 new: bool,
68 },
69}
70
71#[derive(Debug, PartialEq)]
73pub enum PolicyChange<'a> {
74 ThresholdChanged {
76 field: &'static str,
78 old: f64,
80 new: f64,
82 },
83
84 ResponseRulesChanged {
86 old_count: usize,
88 new_count: usize,
90 },
91
92 SuspiciousProcessAdded {
94 pattern: &'a str,
96 },
97
98 SuspiciousProcessRemoved {
100 pattern: &'a str,
102 },
103}
104
105impl Config {
106 pub fn diff<'a>(&'a self, other: &'a Config) -> Result<ConfigDiff<'a>> {
113 let started = Instant::now();
114 let result = (|| {
115 let mut changes = ConfigDiff::new();
116
117 if !self
119 .deception
120 .root_tag
121 .hash_eq_ct(&other.deception.root_tag)
122 {
123 push_config_change(
124 &mut changes,
125 ConfigChange::RootTagChanged {
126 old_hash: hash_prefix_hex(&self.deception.root_tag.hash()[..8])?,
127 new_hash: hash_prefix_hex(&other.deception.root_tag.hash()[..8])?,
128 },
129 )?;
130 }
131
132 for path in &other.deception.decoy_paths {
133 if !self.deception.decoy_paths.iter().any(|current| current == path) {
134 push_config_change(
135 &mut changes,
136 ConfigChange::PathAdded {
137 path: path.as_path(),
138 },
139 )?;
140 }
141 }
142
143 for path in &self.deception.decoy_paths {
144 if !other.deception.decoy_paths.iter().any(|next| next == path) {
145 push_config_change(
146 &mut changes,
147 ConfigChange::PathRemoved {
148 path: path.as_path(),
149 },
150 )?;
151 }
152 }
153
154 if self.telemetry.enable_syscall_monitor != other.telemetry.enable_syscall_monitor {
155 push_config_change(
156 &mut changes,
157 ConfigChange::CapabilitiesChanged {
158 field: "enable_syscall_monitor",
159 old: self.telemetry.enable_syscall_monitor,
160 new: other.telemetry.enable_syscall_monitor,
161 },
162 )?;
163 }
164
165 Ok(changes)
166 })();
167
168 enforce_operation_min_timing(started, TimingOperation::ConfigDiff);
169 result
170 }
171}
172
173impl PolicyConfig {
174 pub fn diff<'a>(&'a self, other: &'a PolicyConfig) -> Result<PolicyDiff<'a>> {
181 let started = Instant::now();
182 let result = (|| {
183 let mut changes = PolicyDiff::new();
184
185 if (self.scoring.alert_threshold - other.scoring.alert_threshold).abs() > 0.01 {
186 push_policy_change(
187 &mut changes,
188 PolicyChange::ThresholdChanged {
189 field: "alert_threshold",
190 old: self.scoring.alert_threshold,
191 new: other.scoring.alert_threshold,
192 },
193 )?;
194 }
195
196 if self.response.rules.len() != other.response.rules.len() {
197 push_policy_change(
198 &mut changes,
199 PolicyChange::ResponseRulesChanged {
200 old_count: self.response.rules.len(),
201 new_count: other.response.rules.len(),
202 },
203 )?;
204 }
205
206 for pattern in &other.deception.suspicious_processes {
207 if !self
208 .deception
209 .suspicious_processes
210 .iter()
211 .any(|current| current == pattern)
212 {
213 push_policy_change(
214 &mut changes,
215 PolicyChange::SuspiciousProcessAdded {
216 pattern: pattern.as_str(),
217 },
218 )?;
219 }
220 }
221
222 for pattern in &self.deception.suspicious_processes {
223 if !other
224 .deception
225 .suspicious_processes
226 .iter()
227 .any(|next| next == pattern)
228 {
229 push_policy_change(
230 &mut changes,
231 PolicyChange::SuspiciousProcessRemoved {
232 pattern: pattern.as_str(),
233 },
234 )?;
235 }
236 }
237
238 Ok(changes)
239 })();
240
241 enforce_operation_min_timing(started, TimingOperation::PolicyDiff);
242 result
243 }
244}
245
246fn push_config_change<'a>(
247 changes: &mut ConfigDiff<'a>,
248 change: ConfigChange<'a>,
249) -> Result<()> {
250 changes.push(change).map_err(|_| {
251 AgentError::new(
252 CFG_VALIDATION_FAILED,
253 "Configuration validation failed",
254 "operation=diff_config; fixed-capacity diff buffer exhausted",
255 "config.diff",
256 )
257 })
258}
259
260fn push_policy_change<'a>(
261 changes: &mut PolicyDiff<'a>,
262 change: PolicyChange<'a>,
263) -> Result<()> {
264 changes.push(change).map_err(|_| {
265 AgentError::new(
266 CFG_VALIDATION_FAILED,
267 "Configuration validation failed",
268 "operation=diff_policy; fixed-capacity diff buffer exhausted",
269 "policy.diff",
270 )
271 })
272}
273
274fn hash_prefix_hex(bytes: &[u8]) -> Result<HString<HASH_PREFIX_HEX_LEN>> {
275 let mut out = HString::<HASH_PREFIX_HEX_LEN>::new();
276 for byte in bytes {
277 write!(&mut out, "{byte:02x}").map_err(|_| {
278 AgentError::new(
279 CFG_VALIDATION_FAILED,
280 "Configuration validation failed",
281 "operation=diff_config; fixed-capacity root-tag hash buffer exhausted",
282 "config.diff",
283 )
284 })?;
285 }
286 Ok(out)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::tags::RootTag;
293
294 #[test]
295 fn test_config_diff_detects_root_tag_change() {
296 let config1 = Config::default();
297 let mut config2 = Config::default();
298 config2.deception.root_tag = RootTag::generate().expect("Failed to generate tag");
299
300 let changes = config1.diff(&config2).expect("diff");
301 assert!(!changes.is_empty());
302
303 if let Some(ConfigChange::RootTagChanged { old_hash, new_hash }) = changes.first() {
304 assert_eq!(old_hash.len(), 16);
305 assert_eq!(new_hash.len(), 16);
306 assert_ne!(old_hash, new_hash);
307 } else {
308 panic!("Expected RootTagChanged");
309 }
310 }
311
312 #[test]
313 fn test_policy_diff_detects_threshold_change() {
314 let mut policy1 = PolicyConfig::default();
315 let mut policy2 = PolicyConfig::default();
316
317 policy1.scoring.alert_threshold = 50.0;
318 policy2.scoring.alert_threshold = 75.0;
319
320 let changes = policy1.diff(&policy2).expect("diff");
321 assert!(!changes.is_empty());
322
323 if let Some(PolicyChange::ThresholdChanged { field, old, new }) = changes.first() {
324 assert_eq!(*field, "alert_threshold");
325 assert_eq!(*old, 50.0);
326 assert_eq!(*new, 75.0);
327 } else {
328 panic!("Expected ThresholdChanged");
329 }
330 }
331}