Skip to main content

palisade_config/
runtime.rs

1//! No-allocation runtime representations derived from deserialized configs.
2//!
3//! Deserialize/serialize can allocate. After conversion to these types, hot-path
4//! operations can run without heap allocations.
5
6use 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
12/// Maximum bytes for path-like fields.
13pub const MAX_PATH_LEN: usize = 512;
14/// Maximum bytes for generic labels.
15pub const MAX_LABEL_LEN: usize = 64;
16/// Maximum number of path entries retained in runtime config.
17pub const MAX_PATH_ENTRIES: usize = 64;
18/// Maximum number of credential types retained in runtime config.
19pub const MAX_CREDENTIAL_TYPES: usize = 32;
20/// Maximum number of suspicious process patterns retained in runtime policy.
21pub const MAX_SUSPICIOUS_PROCESSES: usize = 128;
22/// Maximum number of suspicious artifact patterns retained in runtime policy.
23pub const MAX_SUSPICIOUS_PATTERNS: usize = 128;
24/// Maximum number of registered custom conditions retained in runtime policy.
25pub const MAX_CUSTOM_CONDITIONS: usize = 128;
26
27/// Stack-only runtime configuration for no-allocation operation.
28#[derive(Clone)]
29pub struct RuntimeConfig {
30    /// Effective agent hostname.
31    pub hostname: HString<MAX_LABEL_LEN>,
32    /// Root tag used for derivation.
33    pub root_tag: RootTag,
34    /// Decoy paths (UTF-8 only).
35    pub decoy_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
36    /// Watch paths (UTF-8 only).
37    pub watch_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
38    /// Credential types.
39    pub credential_types: HVec<HString<MAX_LABEL_LEN>, MAX_CREDENTIAL_TYPES>,
40    /// Honeytoken count.
41    pub honeytoken_count: usize,
42    /// Artifact permissions.
43    pub artifact_permissions: u32,
44}
45
46/// Stack-only runtime policy for no-allocation operation.
47#[derive(Clone)]
48pub struct RuntimePolicy {
49    /// Alert threshold.
50    pub alert_threshold: f64,
51    /// Suspicious process patterns.
52    pub suspicious_processes: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PROCESSES>,
53    /// Suspicious artifact patterns.
54    pub suspicious_patterns: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PATTERNS>,
55    /// Registered custom condition names.
56    pub registered_custom_conditions: HVec<HString<MAX_LABEL_LEN>, MAX_CUSTOM_CONDITIONS>,
57}
58
59impl RuntimeConfig {
60    /// Derive an artifact tag hex digest into a caller-provided fixed buffer.
61    ///
62    /// No heap allocation occurs.
63    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    /// Check for suspicious process name using ASCII case-insensitive substring matching.
71    ///
72    /// No heap allocation occurs.
73    #[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    /// Check if a custom condition name is pre-registered.
85    #[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    /// Convert config to a stack-only runtime representation.
99    ///
100    /// # Errors
101    ///
102    /// Returns error if any field exceeds fixed runtime capacity or contains
103    /// non-UTF8 paths.
104    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    /// Convert policy to a stack-only runtime representation.
155    ///
156    /// # Errors
157    ///
158    /// Returns error if any field exceeds fixed runtime capacity.
159    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}