1use crate::errors;
7use crate::timing::{enforce_operation_min_timing, TimingOperation};
8use crate::{AgentError, Config, PolicyConfig, RootTag};
9use heapless::{String as HString, Vec as HVec};
10use std::time::Instant;
11
12pub const MAX_PATH_LEN: usize = 512;
14pub const MAX_LABEL_LEN: usize = 64;
16pub const MAX_PATH_ENTRIES: usize = 64;
18pub const MAX_CREDENTIAL_TYPES: usize = 32;
20pub const MAX_SUSPICIOUS_PROCESSES: usize = 128;
22pub const MAX_SUSPICIOUS_PATTERNS: usize = 128;
24pub const MAX_CUSTOM_CONDITIONS: usize = 128;
26
27#[derive(Clone)]
29pub struct RuntimeConfig {
30 pub hostname: HString<MAX_LABEL_LEN>,
32 pub root_tag: RootTag,
34 pub decoy_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
36 pub watch_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
38 pub credential_types: HVec<HString<MAX_LABEL_LEN>, MAX_CREDENTIAL_TYPES>,
40 pub honeytoken_count: usize,
42 pub artifact_permissions: u32,
44}
45
46#[derive(Clone)]
48pub struct RuntimePolicy {
49 pub alert_threshold: f64,
51 pub suspicious_processes: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PROCESSES>,
53 pub suspicious_patterns: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PATTERNS>,
55 pub registered_custom_conditions: HVec<HString<MAX_LABEL_LEN>, MAX_CUSTOM_CONDITIONS>,
57}
58
59impl RuntimeConfig {
60 pub fn derive_artifact_tag_hex_into(&self, artifact_id: &str, out: &mut [u8; 128]) {
64 self.root_tag
65 .derive_artifact_tag_hex_into(self.hostname.as_str(), artifact_id, out);
66 }
67}
68
69impl RuntimePolicy {
70 #[must_use]
74 pub fn is_suspicious_process(&self, name: &str) -> bool {
75 let started = Instant::now();
76 let found = self
77 .suspicious_processes
78 .iter()
79 .any(|pattern| contains_ascii_case_insensitive(name, pattern.as_str()));
80 enforce_operation_min_timing(started, TimingOperation::PolicySuspiciousCheck);
81 found
82 }
83
84 #[must_use]
86 pub fn is_registered_custom_condition(&self, name: &str) -> bool {
87 let started = Instant::now();
88 let found = self
89 .registered_custom_conditions
90 .iter()
91 .any(|registered| registered.as_str() == name);
92 enforce_operation_min_timing(started, TimingOperation::PolicyCustomConditionCheck);
93 found
94 }
95}
96
97impl Config {
98 pub fn to_runtime(&self) -> Result<RuntimeConfig, AgentError> {
105 let started = Instant::now();
106 let result = (|| {
107 let hostname = push_str::<MAX_LABEL_LEN>("agent.hostname", self.hostname().as_ref())?;
108
109 let mut decoy_paths = HVec::<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>::new();
110 for path in &self.deception.decoy_paths {
111 let path_str = path.to_str().ok_or_else(|| {
112 errors::invalid_value(
113 "to_runtime_config",
114 "deception.decoy_paths",
115 "path must be valid UTF-8 for runtime no-alloc mode",
116 )
117 })?;
118 push_vec_str("deception.decoy_paths", path_str, &mut decoy_paths)?;
119 }
120
121 let mut watch_paths = HVec::<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>::new();
122 for path in &self.telemetry.watch_paths {
123 let path_str = path.to_str().ok_or_else(|| {
124 errors::invalid_value(
125 "to_runtime_config",
126 "telemetry.watch_paths",
127 "path must be valid UTF-8 for runtime no-alloc mode",
128 )
129 })?;
130 push_vec_str("telemetry.watch_paths", path_str, &mut watch_paths)?;
131 }
132
133 let mut credential_types = HVec::<HString<MAX_LABEL_LEN>, MAX_CREDENTIAL_TYPES>::new();
134 for ctype in &self.deception.credential_types {
135 push_vec_str("deception.credential_types", ctype, &mut credential_types)?;
136 }
137
138 Ok(RuntimeConfig {
139 hostname,
140 root_tag: self.deception.root_tag.clone(),
141 decoy_paths,
142 watch_paths,
143 credential_types,
144 honeytoken_count: self.deception.honeytoken_count,
145 artifact_permissions: self.deception.artifact_permissions,
146 })
147 })();
148 enforce_operation_min_timing(started, TimingOperation::RuntimeConfigBuild);
149 result
150 }
151}
152
153impl PolicyConfig {
154 pub fn to_runtime(&self) -> Result<RuntimePolicy, AgentError> {
160 let started = Instant::now();
161 let result = (|| {
162 let mut suspicious_processes =
163 HVec::<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PROCESSES>::new();
164 for p in &self.deception.suspicious_processes {
165 push_vec_str("deception.suspicious_processes", p, &mut suspicious_processes)?;
166 }
167
168 let mut suspicious_patterns =
169 HVec::<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PATTERNS>::new();
170 for p in &self.deception.suspicious_patterns {
171 push_vec_str("deception.suspicious_patterns", p, &mut suspicious_patterns)?;
172 }
173
174 let mut registered_custom_conditions =
175 HVec::<HString<MAX_LABEL_LEN>, MAX_CUSTOM_CONDITIONS>::new();
176 for c in &self.registered_custom_conditions {
177 push_vec_str(
178 "registered_custom_conditions",
179 c,
180 &mut registered_custom_conditions,
181 )?;
182 }
183
184 Ok(RuntimePolicy {
185 alert_threshold: self.scoring.alert_threshold,
186 suspicious_processes,
187 suspicious_patterns,
188 registered_custom_conditions,
189 })
190 })();
191 enforce_operation_min_timing(started, TimingOperation::RuntimePolicyBuild);
192 result
193 }
194}
195
196fn push_str<const N: usize>(field: &str, value: &str) -> Result<HString<N>, AgentError> {
197 let mut out = HString::<N>::new();
198 out.push_str(value).map_err(|_| {
199 errors::invalid_value(
200 "to_runtime",
201 field,
202 format!("value exceeds fixed no-alloc capacity ({N} bytes)"),
203 )
204 })?;
205 Ok(out)
206}
207
208fn push_vec_str<const N: usize, const M: usize>(
209 field: &str,
210 value: &str,
211 out: &mut HVec<HString<N>, M>,
212) -> Result<(), AgentError> {
213 let item = push_str::<N>(field, value)?;
214 out.push(item).map_err(|_| {
215 errors::invalid_value(
216 "to_runtime",
217 field,
218 format!("too many entries for fixed no-alloc capacity ({M})"),
219 )
220 })?;
221 Ok(())
222}
223
224#[inline]
225fn contains_ascii_case_insensitive(haystack: &str, needle: &str) -> bool {
226 if needle.is_empty() {
227 return true;
228 }
229
230 let h = haystack.as_bytes();
231 let n = needle.as_bytes();
232 if n.len() > h.len() {
233 return false;
234 }
235
236 for start in 0..=(h.len() - n.len()) {
237 let mut matched = true;
238 for i in 0..n.len() {
239 if !h[start + i].eq_ignore_ascii_case(&n[i]) {
240 matched = false;
241 break;
242 }
243 }
244 if matched {
245 return true;
246 }
247 }
248 false
249}
250
251#[cfg(test)]
252mod tests {
253 use crate::{Config, PolicyConfig};
254
255 #[test]
256 fn config_to_runtime_works() {
257 let config = Config::default();
258 let rt = config.to_runtime().expect("runtime conversion must succeed");
259 let mut out = [0u8; 128];
260 rt.derive_artifact_tag_hex_into("artifact", &mut out);
261 assert!(out[0].is_ascii_hexdigit());
262 }
263
264 #[test]
265 fn policy_to_runtime_works() {
266 let policy = PolicyConfig::default();
267 let rt = policy.to_runtime().expect("runtime conversion must succeed");
268 assert!(rt.is_suspicious_process("MIMIKATZ.exe"));
269 assert!(!rt.is_suspicious_process("notepad.exe"));
270 }
271}