auths_telemetry/
config.rs1use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use serde::Deserialize;
12use tracing::warn;
13
14use crate::ports::EventSink;
15use crate::sinks::stdout::WriterSink;
16
17#[derive(Debug, Deserialize, Default)]
25pub struct AuditConfig {
26 #[serde(default)]
27 pub sinks: Vec<SinkConfig>,
28}
29
30#[derive(Debug, Deserialize, Clone, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum AuthScheme {
34 Splunk,
36 #[default]
38 Bearer,
39 ApiKeyHeader { header: String },
41}
42
43#[derive(Debug, Deserialize, Clone)]
45#[serde(rename_all = "snake_case")]
46pub enum ConfigPayloadFormat {
47 SplunkHec,
48 DatadogLogs,
49 NdJson,
50}
51
52#[derive(Debug, Deserialize)]
54#[serde(tag = "type")]
55pub enum SinkConfig {
56 #[serde(rename = "http")]
57 Http {
58 url: String,
59 token_env: String,
60 #[serde(default)]
61 auth_scheme: AuthScheme,
62 payload_format: ConfigPayloadFormat,
63 #[serde(default = "default_batch_size")]
64 batch_size: usize,
65 #[serde(default = "default_flush_interval")]
66 flush_interval_ms: u64,
67 },
68 #[serde(rename = "file")]
69 File { path: PathBuf },
70 #[serde(rename = "stdout")]
71 Stdout,
72}
73
74fn default_batch_size() -> usize {
75 10
76}
77
78fn default_flush_interval() -> u64 {
79 5000
80}
81
82pub fn load_audit_config(path: &Path) -> AuditConfig {
95 let content = match fs::read_to_string(path) {
96 Ok(c) => c,
97 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return AuditConfig::default(),
98 Err(e) => {
99 warn!("could not read audit config {}: {e}", path.display());
100 return AuditConfig::default();
101 }
102 };
103
104 match toml::from_str(&content) {
105 Ok(config) => config,
106 Err(e) => {
107 warn!("invalid audit config {}: {e}", path.display());
108 AuditConfig::default()
109 }
110 }
111}
112
113pub fn build_sinks_from_config(
129 config: &AuditConfig,
130 resolve_env: impl Fn(&str) -> Option<String>,
131) -> Vec<Arc<dyn EventSink>> {
132 let mut sinks: Vec<Arc<dyn EventSink>> = Vec::new();
133
134 for sink_config in &config.sinks {
135 match sink_config {
136 SinkConfig::Http { .. } => {
137 build_http_sink(&mut sinks, sink_config, &resolve_env);
138 }
139 SinkConfig::File { path } => {
140 build_file_sink(&mut sinks, path);
141 }
142 SinkConfig::Stdout => {
143 sinks.push(Arc::new(crate::sinks::stdout::new_stdout_sink()));
144 }
145 }
146 }
147
148 sinks
149}
150
151#[cfg(feature = "sink-http")]
152fn build_http_sink(
153 sinks: &mut Vec<Arc<dyn EventSink>>,
154 sink_config: &SinkConfig,
155 resolve_env: &dyn Fn(&str) -> Option<String>,
156) {
157 use std::collections::HashMap;
158
159 use crate::sinks::http::{HttpSink, HttpSinkConfig, PayloadFormat};
160
161 let SinkConfig::Http {
162 url,
163 token_env,
164 auth_scheme,
165 payload_format,
166 batch_size,
167 flush_interval_ms,
168 } = sink_config
169 else {
170 return;
171 };
172
173 let Some(token) = resolve_env(token_env) else {
174 warn!("skipping audit sink: env var '{token_env}' not set");
175 return;
176 };
177
178 let mut headers = HashMap::new();
179 match auth_scheme {
180 AuthScheme::Splunk => {
181 headers.insert("Authorization".to_string(), format!("Splunk {token}"));
182 }
183 AuthScheme::Bearer => {
184 headers.insert("Authorization".to_string(), format!("Bearer {token}"));
185 }
186 AuthScheme::ApiKeyHeader { header } => {
187 headers.insert(header.clone(), token);
188 }
189 }
190
191 let format = match payload_format {
192 ConfigPayloadFormat::SplunkHec => PayloadFormat::SplunkHec,
193 ConfigPayloadFormat::DatadogLogs => PayloadFormat::DatadogLogs,
194 ConfigPayloadFormat::NdJson => PayloadFormat::NdJson,
195 };
196
197 let config = HttpSinkConfig {
198 url: url.to_string(),
199 headers,
200 batch_size: *batch_size,
201 flush_interval_ms: *flush_interval_ms,
202 timeout_ms: 2000,
203 payload_format: format,
204 };
205
206 sinks.push(Arc::new(HttpSink::new(config)));
207}
208
209#[cfg(not(feature = "sink-http"))]
210fn build_http_sink(
211 _sinks: &mut Vec<Arc<dyn EventSink>>,
212 _sink_config: &SinkConfig,
213 _resolve_env: &dyn Fn(&str) -> Option<String>,
214) {
215 warn!("HTTP audit sinks require the 'sink-http' feature; skipping");
216}
217
218fn build_file_sink(sinks: &mut Vec<Arc<dyn EventSink>>, path: &Path) {
219 if let Some(parent) = path.parent()
220 && let Err(e) = fs::create_dir_all(parent)
221 {
222 warn!("could not create directory {}: {e}", parent.display());
223 return;
224 }
225
226 match fs::OpenOptions::new().create(true).append(true).open(path) {
227 Ok(file) => {
228 sinks.push(Arc::new(WriterSink::new(file)));
229 }
230 Err(e) => {
231 warn!("could not open audit log {}: {e}", path.display());
232 }
233 }
234}