use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use config_core::daemon::{
ApplicationProtocolConfig, DEFAULT_TLS_DYNAMIC_EXEC_PLAN_TIMEOUT_MS, EnforcementBackend,
EnforcementConfig, EnforcementDecision, EnforcementMarkStrategy, EnforcementScope,
EnforcementSeccompSyscall, MemlockRlimit, PayloadRedactionPolicy, PayloadSocketCaptureBackend,
PayloadSocketConfig, PayloadSocketSeccompSyscall, PayloadStdioConfig, PayloadStdioStorageMode,
PayloadTlsCaptureBackend, PayloadTlsConfig, PayloadTlsLibrary, PayloadTlsLibraryPath,
PayloadTlsResolver, PayloadTlsSeccompSyscall, PayloadTlsSource,
PayloadTlsSyncRuntimeLibraryPath, ResourceMetricsConfig, SseDataPolicy,
};
use payload_capability::DEFAULT_TLS_SYNC_FLOW_UNKNOWN_STREAM_BYTES;
use crate::args::MmapWorkloadConfig;
pub(super) struct ConfigValues {
values: BTreeMap<String, Vec<String>>,
}
impl ConfigValues {
pub(super) fn parse(raw: &str, allowed_keys: BTreeSet<&'static str>) -> Result<Self, String> {
let mut values = BTreeMap::<String, Vec<String>>::new();
for (line_index, line) in raw.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let (key, value) = trimmed
.split_once('=')
.ok_or_else(|| format!("invalid config line {}", line_index + 1))?;
let key = key.trim().to_string();
let value = unquote(value.trim())?;
if !allowed_keys.contains(key.as_str()) {
return Err(format!("unknown probe config key {key}"));
}
if !matches!(
key.as_str(),
"payload_tls_seccomp_syscall"
| "payload_socket_seccomp_syscall"
| "enforcement_seccomp_syscall"
| "process_seccomp_syscall"
| "agent_invocation_command"
) && values.contains_key(&key)
{
return Err(format!("duplicate config key {key}"));
}
values.entry(key).or_default().push(value);
}
Ok(Self { values })
}
pub(super) fn required(&self, key: &'static str) -> Result<String, String> {
let values = self
.values
.get(key)
.filter(|values| values.len() == 1)
.ok_or_else(|| format!("missing config key {key}"))?;
values
.first()
.cloned()
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("missing config key {key}"))
}
pub(super) fn required_path(&self, key: &'static str) -> Result<PathBuf, String> {
self.required(key).map(PathBuf::from)
}
pub(super) fn required_bool(&self, key: &'static str) -> Result<bool, String> {
match self.required(key)?.as_str() {
"true" => Ok(true),
"false" => Ok(false),
value => Err(format!(
"invalid {key}: expected true or false, got {value}"
)),
}
}
fn optional_bool(&self, key: &'static str, default: bool) -> Result<bool, String> {
let Some(values) = self.values.get(key) else {
return Ok(default);
};
if values.len() != 1 {
return Err(format!("config key {key} must appear once"));
}
match values
.first()
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("missing config key {key}"))?
.as_str()
{
"true" => Ok(true),
"false" => Ok(false),
value => Err(format!(
"invalid {key}: expected true or false, got {value}"
)),
}
}
fn optional_positive_u32(&self, key: &'static str, default: u32) -> Result<u32, String> {
let Some(values) = self.values.get(key) else {
return Ok(default);
};
if values.len() != 1 {
return Err(format!("config key {key} must appear once"));
}
let value = values
.first()
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("missing config key {key}"))?;
let parsed = value
.parse::<u32>()
.map_err(|error| format!("invalid {key}: {error}"))?;
if parsed == 0 {
return Err(format!("invalid {key}: value must be positive"));
}
Ok(parsed)
}
fn optional_positive_u64(&self, key: &'static str, default: u64) -> Result<u64, String> {
let Some(values) = self.values.get(key) else {
return Ok(default);
};
if values.len() != 1 {
return Err(format!("config key {key} must appear once"));
}
let value = values
.first()
.filter(|value| !value.is_empty())
.ok_or_else(|| format!("missing config key {key}"))?;
let parsed = value
.parse::<u64>()
.map_err(|error| format!("invalid {key}: {error}"))?;
if parsed == 0 {
return Err(format!("invalid {key}: value must be positive"));
}
Ok(parsed)
}
fn required_u32(&self, key: &'static str) -> Result<u32, String> {
self.required(key)?
.parse::<u32>()
.map_err(|error| format!("invalid {key}: {error}"))
}
pub(super) fn required_positive_u32(&self, key: &'static str) -> Result<u32, String> {
let value = self.required_u32(key)?;
if value == u32::default() {
return Err(format!("invalid {key}: value must be positive"));
}
Ok(value)
}
pub(super) fn required_u64(&self, key: &'static str) -> Result<u64, String> {
self.required(key)?
.parse::<u64>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_positive_u64(&self, key: &'static str) -> Result<u64, String> {
let value = self.required_u64(key)?;
if value == u64::default() {
return Err(format!("invalid {key}: value must be positive"));
}
Ok(value)
}
fn required_disabled_or_positive_u64(&self, key: &'static str) -> Result<Option<u64>, String> {
let raw = self.required(key)?;
if raw == "disabled" {
return Ok(None);
}
let value = raw
.parse::<u64>()
.map_err(|error| format!("invalid {key}: {error}"))?;
if value == u64::default() {
return Err(format!("invalid {key}: value must be positive or disabled"));
}
Ok(Some(value))
}
pub(super) fn required_octal(&self, key: &'static str) -> Result<u32, String> {
u32::from_str_radix(&self.required(key)?, 8)
.map_err(|error| format!("invalid {key}: {error}"))
}
pub(super) fn required_memlock_rlimit(
&self,
key: &'static str,
) -> Result<MemlockRlimit, String> {
self.required(key)?
.parse::<MemlockRlimit>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_tls_library(&self, key: &'static str) -> Result<PayloadTlsLibrary, String> {
self.required(key)?
.parse::<PayloadTlsLibrary>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_tls_source(&self, key: &'static str) -> Result<PayloadTlsSource, String> {
self.required(key)?
.parse::<PayloadTlsSource>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_tls_resolver(
&self,
key: &'static str,
) -> Result<PayloadTlsResolver, String> {
self.required(key)?
.parse::<PayloadTlsResolver>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_tls_capture_backend(
&self,
key: &'static str,
) -> Result<PayloadTlsCaptureBackend, String> {
self.required(key)?
.parse::<PayloadTlsCaptureBackend>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn payload_tls_seccomp_syscalls(&self) -> Result<Vec<PayloadTlsSeccompSyscall>, String> {
let Some(values) = self.values.get("payload_tls_seccomp_syscall") else {
return Err("missing config key payload_tls_seccomp_syscall".to_string());
};
values
.iter()
.map(|value| {
value
.parse::<PayloadTlsSeccompSyscall>()
.map_err(|error| format!("invalid payload_tls_seccomp_syscall: {error}"))
})
.collect()
}
fn required_payload_socket_capture_backend(
&self,
key: &'static str,
) -> Result<PayloadSocketCaptureBackend, String> {
self.required(key)?
.parse::<PayloadSocketCaptureBackend>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn payload_socket_seccomp_syscalls(&self) -> Result<Vec<PayloadSocketSeccompSyscall>, String> {
let Some(values) = self.values.get("payload_socket_seccomp_syscall") else {
return Err("missing config key payload_socket_seccomp_syscall".to_string());
};
values
.iter()
.map(|value| {
value
.parse::<PayloadSocketSeccompSyscall>()
.map_err(|error| format!("invalid payload_socket_seccomp_syscall: {error}"))
})
.collect()
}
fn enforcement_seccomp_syscalls(&self) -> Result<Vec<EnforcementSeccompSyscall>, String> {
let Some(values) = self.values.get("enforcement_seccomp_syscall") else {
return Err("missing config key enforcement_seccomp_syscall".to_string());
};
values
.iter()
.map(|value| {
value
.parse::<EnforcementSeccompSyscall>()
.map_err(|error| format!("invalid enforcement_seccomp_syscall: {error}"))
})
.collect()
}
fn required_payload_tls_library_path(
&self,
key: &'static str,
) -> Result<PayloadTlsLibraryPath, String> {
self.required(key)?
.parse::<PayloadTlsLibraryPath>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_tls_sync_runtime_library_path(
&self,
key: &'static str,
) -> Result<PayloadTlsSyncRuntimeLibraryPath, String> {
self.required(key)?
.parse::<PayloadTlsSyncRuntimeLibraryPath>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_disabled_or_path(
&self,
key: &'static str,
) -> Result<config_core::daemon::DisabledOrPath, String> {
self.required(key)?
.parse::<config_core::daemon::DisabledOrPath>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_redaction_policy(
&self,
key: &'static str,
) -> Result<PayloadRedactionPolicy, String> {
self.required(key)?
.parse::<PayloadRedactionPolicy>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_payload_stdio_storage_mode(
&self,
key: &'static str,
) -> Result<PayloadStdioStorageMode, String> {
self.required(key)?
.parse::<PayloadStdioStorageMode>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_sse_data_policy(&self, key: &'static str) -> Result<SseDataPolicy, String> {
self.required(key)?
.parse::<SseDataPolicy>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_enforcement_backend(
&self,
key: &'static str,
) -> Result<EnforcementBackend, String> {
self.required(key)?
.parse::<EnforcementBackend>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_enforcement_scope(&self, key: &'static str) -> Result<EnforcementScope, String> {
self.required(key)?
.parse::<EnforcementScope>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_enforcement_decision(
&self,
key: &'static str,
) -> Result<EnforcementDecision, String> {
self.required(key)?
.parse::<EnforcementDecision>()
.map_err(|error| format!("invalid {key}: {error}"))
}
fn required_enforcement_mark_strategy(
&self,
key: &'static str,
) -> Result<EnforcementMarkStrategy, String> {
self.required(key)?
.parse::<EnforcementMarkStrategy>()
.map_err(|error| format!("invalid {key}: {error}"))
}
pub(super) fn payload_tls_config(&self) -> Result<PayloadTlsConfig, String> {
Ok(PayloadTlsConfig {
enabled: self.required_bool("payload_tls_enabled")?,
capture_backend: self
.required_payload_tls_capture_backend("payload_tls_capture_backend")?,
source: self.required_payload_tls_source("payload_tls_source")?,
resolver: self.required_payload_tls_resolver("payload_tls_resolver")?,
library: self.required_payload_tls_library("payload_tls_library")?,
library_path: self.required_payload_tls_library_path("payload_tls_library_path")?,
binary_path: self.required_disabled_or_path("payload_tls_binary_path")?,
pattern_path: self.required_disabled_or_path("payload_tls_pattern_path")?,
max_segment_bytes: self.required_positive_u32("payload_tls_max_segment_bytes")?,
max_operation_bytes: self.required_positive_u32("payload_tls_max_operation_bytes")?,
ring_buffer_bytes: self.required_positive_u32("payload_tls_ring_buffer_bytes")?,
pending_operation_max_entries: self
.required_positive_u32("payload_tls_pending_operation_max_entries")?,
seccomp_syscalls: self.payload_tls_seccomp_syscalls()?,
diagnostics_enabled: self.optional_bool("payload_tls_diagnostics_enabled", false)?,
retention_max_bytes_per_trace: self
.required_positive_u64("payload_tls_retention_max_bytes_per_trace")?,
redaction_policy: self
.required_payload_redaction_policy("payload_tls_redaction_policy")?,
sync_runtime_library_path: self.required_payload_tls_sync_runtime_library_path(
"payload_tls_sync_runtime_library_path",
)?,
sync_event_socket_path: self.required_path("payload_tls_sync_event_socket_path")?,
sync_socket_mode: self.required_octal("payload_tls_sync_socket_mode_octal")?,
sync_match_limit: self.required_positive_u32("payload_tls_sync_match_limit")?,
dynamic_exec_plan_timeout_ms: DEFAULT_TLS_DYNAMIC_EXEC_PLAN_TIMEOUT_MS,
sync_flow_control_enabled: self
.optional_bool("payload_tls_sync_flow_control_enabled", true)?,
sync_flow_sniff_bytes: self
.optional_positive_u32("payload_tls_sync_flow_sniff_bytes", 65536)?,
sync_flow_max_header_bytes: self
.optional_positive_u32("payload_tls_sync_flow_max_header_bytes", 16384)?,
sync_flow_large_transfer_bytes: self
.optional_positive_u64("payload_tls_sync_flow_large_transfer_bytes", 1048576)?,
sync_flow_unknown_stream_bytes: self.optional_positive_u64(
"payload_tls_sync_flow_unknown_stream_bytes",
DEFAULT_TLS_SYNC_FLOW_UNKNOWN_STREAM_BYTES,
)?,
sync_flow_h2_data_probe_bytes: self
.optional_positive_u64("payload_tls_sync_flow_h2_data_probe_bytes", 65536)?,
java_agent_enabled: self.optional_bool("payload_tls_java_agent_enabled", false)?,
})
}
pub(super) fn payload_stdio_config(&self) -> Result<PayloadStdioConfig, String> {
Ok(PayloadStdioConfig {
enabled: self.required_bool("payload_stdio_enabled")?,
capture_stdin: self.required_bool("payload_stdio_capture_stdin")?,
capture_stdout: self.required_bool("payload_stdio_capture_stdout")?,
capture_stderr: self.required_bool("payload_stdio_capture_stderr")?,
stdin_storage_mode: self
.required_payload_stdio_storage_mode("payload_stdio_stdin_storage_mode")?,
stdout_storage_mode: self
.required_payload_stdio_storage_mode("payload_stdio_stdout_storage_mode")?,
stderr_storage_mode: self
.required_payload_stdio_storage_mode("payload_stdio_stderr_storage_mode")?,
max_segment_bytes: self.required_positive_u32("payload_stdio_max_segment_bytes")?,
ring_buffer_bytes: self.required_positive_u32("payload_stdio_ring_buffer_bytes")?,
pending_operation_max_entries: self
.required_positive_u32("payload_stdio_pending_operation_max_entries")?,
stream_state_max_entries: self
.required_positive_u32("payload_stdio_stream_state_max_entries")?,
retention_max_bytes_per_trace: self
.required_positive_u64("payload_stdio_retention_max_bytes_per_trace")?,
redaction_policy: self
.required_payload_redaction_policy("payload_stdio_redaction_policy")?,
})
}
pub(super) fn payload_socket_config(&self) -> Result<PayloadSocketConfig, String> {
Ok(PayloadSocketConfig {
enabled: self.required_bool("payload_socket_enabled")?,
capture_backend: self
.required_payload_socket_capture_backend("payload_socket_capture_backend")?,
max_segment_bytes: self.required_positive_u32("payload_socket_max_segment_bytes")?,
max_operation_bytes: self
.required_positive_u32("payload_socket_max_operation_bytes")?,
ring_buffer_bytes: self.required_positive_u32("payload_socket_ring_buffer_bytes")?,
pending_operation_max_entries: self
.required_positive_u32("payload_socket_pending_operation_max_entries")?,
stream_state_max_entries: self
.required_positive_u32("payload_socket_stream_state_max_entries")?,
retention_max_bytes_per_trace: self
.required_positive_u64("payload_socket_retention_max_bytes_per_trace")?,
redaction_policy: self
.required_payload_redaction_policy("payload_socket_redaction_policy")?,
http_sniff_max_bytes: self
.required_positive_u64("payload_socket_http_sniff_max_bytes")?,
seccomp_syscalls: self.payload_socket_seccomp_syscalls()?,
})
}
pub(super) fn application_protocol_config(&self) -> Result<ApplicationProtocolConfig, String> {
Ok(ApplicationProtocolConfig {
enabled: self.required_bool("application_protocol_enabled")?,
http1_enabled: self.required_bool("application_protocol_http1_enabled")?,
http2_enabled: self.required_bool("application_protocol_http2_enabled")?,
capture_host: self.required_bool("application_http_capture_host")?,
sse_enabled: self.required_bool("application_http_sse_enabled")?,
sse_data_policy: self.required_sse_data_policy("application_http_sse_data_policy")?,
sse_max_buffer_bytes: self
.required_positive_u64("application_http_sse_max_buffer_bytes")?,
sse_max_data_bytes: self
.required_positive_u64("application_http_sse_max_data_bytes")?,
http2_max_frame_bytes: self
.required_positive_u64("application_http2_max_frame_bytes")?,
http2_max_connection_buffer_bytes: self
.required_positive_u64("application_http2_max_connection_buffer_bytes")?,
http2_emit_data_preview: self.required_bool("application_http2_emit_data_preview")?,
http2_max_data_preview_bytes: self
.required_positive_u64("application_http2_max_data_preview_bytes")?,
})
}
pub(super) fn resource_metrics_config(&self) -> Result<ResourceMetricsConfig, String> {
Ok(ResourceMetricsConfig {
enabled: self.required_bool("resource_metrics_enabled")?,
interval_ms: self.required_positive_u64("resource_metrics_interval_ms")?,
include_children: self.required_bool("resource_metrics_include_children")?,
include_system: self.required_bool("resource_metrics_include_system")?,
cpu_alert_percent_millis: self
.required_disabled_or_positive_u64("resource_metrics_cpu_alert_percent_millis")?,
memory_alert_rss_kb: self
.required_disabled_or_positive_u64("resource_metrics_memory_alert_rss_kb")?,
})
}
pub(super) fn enforcement_config(&self) -> Result<EnforcementConfig, String> {
Ok(EnforcementConfig {
enabled: self.required_bool("enforcement_enabled")?,
backend: self.required_enforcement_backend("enforcement_backend")?,
scope: self.required_enforcement_scope("enforcement_scope")?,
rules_path: self.required_path("enforcement_rules_path")?,
builtin_rules: Vec::new(),
default_decision: self.required_enforcement_decision("enforcement_default_decision")?,
mark_strategy: self.required_enforcement_mark_strategy("enforcement_mark_strategy")?,
audit_enabled: self.required_bool("enforcement_audit_enabled")?,
event_buffer_bytes: self.required_positive_u32("enforcement_event_buffer_bytes")?,
seccomp_syscalls: self.enforcement_seccomp_syscalls()?,
seccomp_path_max_bytes: self
.required_positive_u32("enforcement_seccomp_path_max_bytes")?,
})
}
pub(super) fn optional_mmap_config(&self) -> Result<Option<MmapWorkloadConfig>, String> {
if !self.required_bool("mmap_enabled")? {
self.reject_disabled_mmap_keys()?;
return Ok(None);
}
Ok(Some(MmapWorkloadConfig {
path: self.required_path("mmap_path")?,
message: self.required("mmap_message")?,
length: self.required_positive_u64("mmap_length")?,
offset: self.required_u64("mmap_offset")?,
}))
}
fn reject_disabled_mmap_keys(&self) -> Result<(), String> {
for key in ["mmap_path", "mmap_message", "mmap_length", "mmap_offset"] {
if self.values.contains_key(key) {
return Err(format!("{key} is only valid when mmap_enabled = true"));
}
}
Ok(())
}
}
fn unquote(value: &str) -> Result<String, String> {
if value.starts_with('"') || value.ends_with('"') {
if !(value.starts_with('"') && value.ends_with('"') && value.len() >= 2) {
return Err(format!("invalid quoted value {value}"));
}
return Ok(value[1..value.len() - 1].to_string());
}
Ok(value.to_string())
}