use crate::timing::{TimingOperation, enforce_operation_min_timing};
use crate::{AgentError, Config, PolicyConfig, RootTag};
use heapless::{String as HString, Vec as HVec};
use std::time::Instant;
const CFG_INVALID_VALUE: u16 = 103;
pub const MAX_PATH_LEN: usize = 512;
pub const MAX_LABEL_LEN: usize = 64;
pub const MAX_PATH_ENTRIES: usize = 64;
pub const MAX_CREDENTIAL_TYPES: usize = 32;
pub const MAX_SUSPICIOUS_PROCESSES: usize = 128;
pub const MAX_SUSPICIOUS_PATTERNS: usize = 128;
pub const MAX_CUSTOM_CONDITIONS: usize = 128;
pub struct RuntimeConfig {
pub hostname: HString<MAX_LABEL_LEN>,
host_tag: [u8; 64],
pub decoy_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
pub watch_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
pub credential_types: HVec<HString<MAX_LABEL_LEN>, MAX_CREDENTIAL_TYPES>,
pub honeytoken_count: usize,
pub artifact_permissions: u32,
}
pub struct RuntimePolicy {
pub alert_threshold: f64,
pub suspicious_processes: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PROCESSES>,
pub suspicious_patterns: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PATTERNS>,
pub registered_custom_conditions: HVec<HString<MAX_LABEL_LEN>, MAX_CUSTOM_CONDITIONS>,
}
impl RuntimeConfig {
pub(crate) fn from_parts(
hostname: HString<MAX_LABEL_LEN>,
host_tag: [u8; 64],
decoy_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
watch_paths: HVec<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>,
credential_types: HVec<HString<MAX_LABEL_LEN>, MAX_CREDENTIAL_TYPES>,
honeytoken_count: usize,
artifact_permissions: u32,
) -> Self {
Self {
hostname,
host_tag,
decoy_paths,
watch_paths,
credential_types,
honeytoken_count,
artifact_permissions,
}
}
pub fn derive_artifact_tag_hex_into(&self, artifact_id: &str, out: &mut [u8; 128]) {
RootTag::derive_artifact_tag_hex_from_host_tag_into(&self.host_tag, artifact_id, out);
}
}
impl RuntimePolicy {
pub(crate) fn from_parts(
alert_threshold: f64,
suspicious_processes: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PROCESSES>,
suspicious_patterns: HVec<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PATTERNS>,
registered_custom_conditions: HVec<HString<MAX_LABEL_LEN>, MAX_CUSTOM_CONDITIONS>,
) -> Self {
Self {
alert_threshold,
suspicious_processes,
suspicious_patterns,
registered_custom_conditions,
}
}
#[must_use]
pub fn is_suspicious_process(&self, name: &str) -> bool {
let started = Instant::now();
let found = self
.suspicious_processes
.iter()
.any(|pattern| contains_ascii_case_insensitive(name, pattern.as_str()));
enforce_operation_min_timing(started, TimingOperation::PolicySuspiciousCheck);
found
}
#[must_use]
pub fn is_registered_custom_condition(&self, name: &str) -> bool {
let started = Instant::now();
let found = self
.registered_custom_conditions
.iter()
.any(|registered| registered.as_str() == name);
enforce_operation_min_timing(started, TimingOperation::PolicyCustomConditionCheck);
found
}
}
impl Config {
pub fn to_runtime(&self) -> Result<RuntimeConfig, AgentError> {
let started = Instant::now();
let result = (|| {
let hostname = push_str::<MAX_LABEL_LEN>("agent.hostname", self.hostname().as_ref())?;
let host_tag = self.deception.root_tag.derive_host_tag_bytes(hostname.as_str());
let mut decoy_paths = HVec::<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>::new();
for path in &self.deception.decoy_paths {
let path_str = path.to_str().ok_or_else(|| {
AgentError::new(
CFG_INVALID_VALUE,
"Configuration contains an invalid value",
"operation=to_runtime_config; field=deception.decoy_paths; path must be valid UTF-8 for runtime no-alloc mode",
"deception.decoy_paths",
)
})?;
push_vec_str("deception.decoy_paths", path_str, &mut decoy_paths)?;
}
let mut watch_paths = HVec::<HString<MAX_PATH_LEN>, MAX_PATH_ENTRIES>::new();
for path in &self.telemetry.watch_paths {
let path_str = path.to_str().ok_or_else(|| {
AgentError::new(
CFG_INVALID_VALUE,
"Configuration contains an invalid value",
"operation=to_runtime_config; field=telemetry.watch_paths; path must be valid UTF-8 for runtime no-alloc mode",
"telemetry.watch_paths",
)
})?;
push_vec_str("telemetry.watch_paths", path_str, &mut watch_paths)?;
}
let mut credential_types = HVec::<HString<MAX_LABEL_LEN>, MAX_CREDENTIAL_TYPES>::new();
for ctype in &self.deception.credential_types {
push_vec_str("deception.credential_types", ctype, &mut credential_types)?;
}
Ok(RuntimeConfig {
hostname,
host_tag,
decoy_paths,
watch_paths,
credential_types,
honeytoken_count: self.deception.honeytoken_count,
artifact_permissions: self.deception.artifact_permissions,
})
})();
enforce_operation_min_timing(started, TimingOperation::RuntimeConfigBuild);
result
}
}
impl PolicyConfig {
pub fn to_runtime(&self) -> Result<RuntimePolicy, AgentError> {
let started = Instant::now();
let result = (|| {
let mut suspicious_processes =
HVec::<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PROCESSES>::new();
for p in &self.deception.suspicious_processes {
push_vec_str(
"deception.suspicious_processes",
p,
&mut suspicious_processes,
)?;
}
let mut suspicious_patterns =
HVec::<HString<MAX_LABEL_LEN>, MAX_SUSPICIOUS_PATTERNS>::new();
for p in &self.deception.suspicious_patterns {
push_vec_str("deception.suspicious_patterns", p, &mut suspicious_patterns)?;
}
let mut registered_custom_conditions =
HVec::<HString<MAX_LABEL_LEN>, MAX_CUSTOM_CONDITIONS>::new();
for c in &self.registered_custom_conditions {
push_vec_str(
"registered_custom_conditions",
c,
&mut registered_custom_conditions,
)?;
}
Ok(RuntimePolicy {
alert_threshold: self.scoring.alert_threshold,
suspicious_processes,
suspicious_patterns,
registered_custom_conditions,
})
})();
enforce_operation_min_timing(started, TimingOperation::RuntimePolicyBuild);
result
}
}
fn push_str<const N: usize>(field: &str, value: &str) -> Result<HString<N>, AgentError> {
let mut out = HString::<N>::new();
out.push_str(value).map_err(|_| {
AgentError::new(
CFG_INVALID_VALUE,
"Configuration contains an invalid value",
format!(
"operation=to_runtime; field={field}; value exceeds fixed no-alloc capacity ({N} bytes)"
),
field,
)
})?;
Ok(out)
}
fn push_vec_str<const N: usize, const M: usize>(
field: &str,
value: &str,
out: &mut HVec<HString<N>, M>,
) -> Result<(), AgentError> {
let item = push_str::<N>(field, value)?;
out.push(item).map_err(|_| {
AgentError::new(
CFG_INVALID_VALUE,
"Configuration contains an invalid value",
format!("operation=to_runtime; field={field}; too many entries for fixed no-alloc capacity ({M})"),
field,
)
})?;
Ok(())
}
#[inline]
fn contains_ascii_case_insensitive(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
let h = haystack.as_bytes();
let n = needle.as_bytes();
if n.len() > h.len() {
return false;
}
for start in 0..=(h.len() - n.len()) {
let mut matched = true;
for i in 0..n.len() {
if !h[start + i].eq_ignore_ascii_case(&n[i]) {
matched = false;
break;
}
}
if matched {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use crate::{Config, PolicyConfig};
#[test]
fn config_to_runtime_works() {
let config = Config::default();
let rt = config
.to_runtime()
.expect("runtime conversion must succeed");
let mut out = [0u8; 128];
rt.derive_artifact_tag_hex_into("artifact", &mut out);
assert!(out[0].is_ascii_hexdigit());
}
#[test]
fn policy_to_runtime_works() {
let policy = PolicyConfig::default();
let rt = policy
.to_runtime()
.expect("runtime conversion must succeed");
assert!(rt.is_suspicious_process("MIMIKATZ.exe"));
assert!(!rt.is_suspicious_process("notepad.exe"));
}
}