1use std::collections::{HashMap, HashSet};
2
3use crate::audit::{AuditConfig, AuditLevel, Module};
4
5const ALLOWED_ENV_VARS: &[&str] = &[
10 "TEAQL_AUDIT",
11 "TEAQL_SQL",
12 "TEAQL_SQL_TABLES",
13 "TEAQL_TOOL",
14 "TEAQL_TOOL_FOCUS",
15 "TEAQL_SINK",
16 "TEAQL_SCHEMA",
17];
18
19const ALLOWED_LEVELS: &[&str] = &["_silent", "_summary", "_full"];
22
23const ALLOWED_SINKS: &[&str] = &["_stdout", "_file", "_both"];
25
26const ALLOWED_SCHEMA_MODES: &[&str] = &["_verify", "_dryrun", "_execute"];
28
29const ALLOWED_MODULES: &[(&str, Module)] = &[
31 ("http", Module::Http),
32 ("file", Module::File),
33 ("cmd", Module::Cmd),
34 ("email", Module::Email),
35 ("kv", Module::Kv),
36 ("crypto", Module::Crypto),
37 ("jwt", Module::Jwt),
38 ("time", Module::Time),
39 ("id", Module::Id),
40 ("text", Module::Text),
41 ("decimal", Module::Decimal),
42 ("money", Module::Money),
43 ("json", Module::Json),
44 ("regex", Module::Regex),
45 ("codec", Module::Codec),
46 ("list", Module::List),
47 ("map", Module::Map),
48 ("diff", Module::Diff),
49 ("url", Module::Url),
50 ("validate", Module::Validate),
51 ("color", Module::Color),
52 ("unit", Module::Unit),
53 ("daterange", Module::DateRange),
54 ("desensitize", Module::Desensitize),
55 ("filter", Module::Filter),
56 ("tree", Module::Tree),
57 ("system", Module::System),
58];
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum AuditSink {
65 Stdout,
66 File,
67 Both,
68}
69
70impl Default for AuditSink {
71 fn default() -> Self {
72 AuditSink::Both
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum SchemaMode {
80 Verify,
84
85 DryRun,
88
89 Execute,
92}
93
94impl Default for SchemaMode {
95 fn default() -> Self {
96 SchemaMode::Verify
97 }
98}
99
100#[derive(Debug, Clone)]
105pub struct EnvAuditConfig {
106 pub config: AuditConfig,
108 pub entity_level: AuditLevel,
110 pub sql_level: AuditLevel,
112 pub sql_tables: Option<HashSet<String>>,
114 pub sink: AuditSink,
116 pub schema_mode: SchemaMode,
118}
119
120impl EnvAuditConfig {
121 pub fn sql_active_for(&self, table: &str) -> bool {
123 if self.sql_level == AuditLevel::Silent {
124 return false;
125 }
126 match &self.sql_tables {
127 Some(tables) => tables.contains(table),
128 None => true, }
130 }
131}
132
133fn parse_level(value: &str, var_name: &str) -> AuditLevel {
136 match value {
137 "_silent" => AuditLevel::Silent,
138 "_summary" => AuditLevel::Summary,
139 "_full" => AuditLevel::Full,
140 other => {
141 eprintln!(
142 "\nFATAL: Invalid value \"{}\" for environment variable \"{}\"\n\
143 Allowed values: {}\n\n\
144 Application refused to start.\n",
145 other,
146 var_name,
147 ALLOWED_LEVELS.join(", "),
148 );
149 std::process::exit(1);
150 }
151 }
152}
153
154fn parse_sink(value: &str) -> AuditSink {
155 match value {
156 "_stdout" => AuditSink::Stdout,
157 "_file" => AuditSink::File,
158 "_both" => AuditSink::Both,
159 other => {
160 eprintln!(
161 "\nFATAL: Invalid value \"{}\" for environment variable \"TEAQL_SINK\"\n\
162 Allowed values: {}\n\n\
163 Application refused to start.\n",
164 other,
165 ALLOWED_SINKS.join(", "),
166 );
167 std::process::exit(1);
168 }
169 }
170}
171
172fn parse_schema_mode(value: &str) -> SchemaMode {
173 match value {
174 "_verify" => SchemaMode::Verify,
175 "_dryrun" => SchemaMode::DryRun,
176 "_execute" => SchemaMode::Execute,
177 other => {
178 eprintln!(
179 "\nFATAL: Invalid value \"{}\" for environment variable \"TEAQL_SCHEMA\"\n\
180 Allowed values: {}\n\n\
181 Application refused to start.\n",
182 other,
183 ALLOWED_SCHEMA_MODES.join(", "),
184 );
185 std::process::exit(1);
186 }
187 }
188}
189
190fn parse_module_list(value: &str) -> Vec<Module> {
191 let module_map: HashMap<&str, Module> = ALLOWED_MODULES.iter().copied().collect();
192 let available: Vec<&str> = ALLOWED_MODULES.iter().map(|(name, _)| *name).collect();
193
194 value
195 .split(',')
196 .map(str::trim)
197 .filter(|s| !s.is_empty())
198 .map(|name| {
199 *module_map.get(name).unwrap_or_else(|| {
200 eprintln!(
201 "\nFATAL: Unknown module \"{}\" in TEAQL_TOOL_FOCUS\n\
202 Available modules: {}\n\n\
203 Application refused to start.\n",
204 name,
205 available.join(", "),
206 );
207 std::process::exit(1);
208 })
209 })
210 .collect()
211}
212
213fn parse_table_list(value: &str, known_tables: &[&str]) -> HashSet<String> {
214 let known_set: HashSet<&str> = known_tables.iter().copied().collect();
215 value
216 .split(',')
217 .map(str::trim)
218 .filter(|s| !s.is_empty())
219 .map(|name| {
220 if !known_set.contains(name) {
221 eprintln!(
222 "\nFATAL: Unknown table \"{}\" in TEAQL_SQL_TABLES\n\
223 Available tables: {}\n\n\
224 Application refused to start.\n",
225 name,
226 known_tables.join(", "),
227 );
228 std::process::exit(1);
229 }
230 name.to_string()
231 })
232 .collect()
233}
234
235const AUDIT_PREFIXES: &[&str] = &[
241 "TEAQL_AUDIT",
242 "TEAQL_SQL",
243 "TEAQL_TOOL",
244 "TEAQL_SINK",
245 "TEAQL_SCHEMA",
246];
247
248fn enforce_env_whitelist() {
253 let allowed: HashSet<&str> = ALLOWED_ENV_VARS.iter().copied().collect();
254
255 for (key, _) in std::env::vars() {
256 let in_audit_namespace = AUDIT_PREFIXES.iter().any(|prefix| key.starts_with(prefix));
257 if in_audit_namespace && !allowed.contains(key.as_str()) {
258 let suggestion = ALLOWED_ENV_VARS
260 .iter()
261 .min_by_key(|v| levenshtein(&key, v))
262 .unwrap();
263
264 eprintln!(
265 "\nFATAL: Unknown environment variable \"{}\"\n\
266 Did you mean \"{}\"?\n\n\
267 Allowed TEAQL audit variables:\n {}\n\n\
268 Application refused to start.\n",
269 key,
270 suggestion,
271 ALLOWED_ENV_VARS.join(", "),
272 );
273 std::process::exit(1);
274 }
275 }
276}
277
278fn levenshtein(a: &str, b: &str) -> usize {
280 let a: Vec<char> = a.chars().collect();
281 let b: Vec<char> = b.chars().collect();
282 let (m, n) = (a.len(), b.len());
283 let mut dp = vec![vec![0usize; n + 1]; m + 1];
284 for i in 0..=m {
285 dp[i][0] = i;
286 }
287 for j in 0..=n {
288 dp[0][j] = j;
289 }
290 for i in 1..=m {
291 for j in 1..=n {
292 let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
293 dp[i][j] = (dp[i - 1][j] + 1)
294 .min(dp[i][j - 1] + 1)
295 .min(dp[i - 1][j - 1] + cost);
296 }
297 }
298 dp[m][n]
299}
300
301pub fn audit_config_from_env(known_tables: &[&str]) -> EnvAuditConfig {
316 enforce_env_whitelist();
318
319 let entity_level = std::env::var("TEAQL_AUDIT")
321 .map(|v| parse_level(&v, "TEAQL_AUDIT"))
322 .unwrap_or(AuditLevel::Full);
323
324 let sql_level = std::env::var("TEAQL_SQL")
325 .map(|v| parse_level(&v, "TEAQL_SQL"))
326 .unwrap_or(AuditLevel::Silent);
327
328 let sql_tables: Option<HashSet<String>> = std::env::var("TEAQL_SQL_TABLES")
329 .ok()
330 .map(|v| parse_table_list(&v, known_tables));
331
332 let tool_level = std::env::var("TEAQL_TOOL")
333 .map(|v| parse_level(&v, "TEAQL_TOOL"))
334 .unwrap_or(AuditLevel::Silent);
335
336 let tool_focus: Option<Vec<Module>> = std::env::var("TEAQL_TOOL_FOCUS")
337 .ok()
338 .map(|v| parse_module_list(&v));
339
340 let sink = std::env::var("TEAQL_SINK")
341 .map(|v| parse_sink(&v))
342 .unwrap_or(AuditSink::Both);
343
344 let schema_mode = std::env::var("TEAQL_SCHEMA")
345 .map(|v| parse_schema_mode(&v))
346 .unwrap_or(SchemaMode::Verify);
347
348 let config = match &tool_focus {
350 Some(focused) => {
351 let mut cfg = AuditConfig::new(tool_level, tool_level);
353 for m in focused {
354 cfg = cfg.enable(*m, AuditLevel::Full);
355 }
356 cfg
357 }
358 None => {
359 AuditConfig::new(tool_level, tool_level)
361 }
362 };
363
364 EnvAuditConfig {
365 config,
366 entity_level,
367 sql_level,
368 sql_tables,
369 sink,
370 schema_mode,
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_levenshtein() {
380 assert_eq!(levenshtein("TEAQL_SQL", "TEAQL_SQL"), 0);
381 assert_eq!(levenshtein("TEAQL_SQLL", "TEAQL_SQL"), 1);
382 assert_eq!(levenshtein("TEAQL_AUDIT", "TEAQL_SQL"), 5);
383 }
384
385 #[test]
386 fn test_parse_level() {
387 assert_eq!(parse_level("_silent", "TEST"), AuditLevel::Silent);
388 assert_eq!(parse_level("_summary", "TEST"), AuditLevel::Summary);
389 assert_eq!(parse_level("_full", "TEST"), AuditLevel::Full);
390 }
391
392 #[test]
393 fn test_parse_sink() {
394 assert_eq!(parse_sink("_stdout"), AuditSink::Stdout);
395 assert_eq!(parse_sink("_file"), AuditSink::File);
396 assert_eq!(parse_sink("_both"), AuditSink::Both);
397 }
398
399 #[test]
400 fn test_parse_schema_mode() {
401 assert_eq!(parse_schema_mode("_verify"), SchemaMode::Verify);
402 assert_eq!(parse_schema_mode("_dryrun"), SchemaMode::DryRun);
403 assert_eq!(parse_schema_mode("_execute"), SchemaMode::Execute);
404 assert_eq!(SchemaMode::default(), SchemaMode::Verify);
405 }
406
407 #[test]
408 fn test_parse_module_list() {
409 let modules = parse_module_list("http,money,crypto");
410 assert_eq!(modules.len(), 3);
411 assert_eq!(modules[0], Module::Http);
412 assert_eq!(modules[1], Module::Money);
413 assert_eq!(modules[2], Module::Crypto);
414 }
415
416 #[test]
417 fn test_parse_table_list() {
418 let tables = parse_table_list("task,task_status", &["task", "task_status", "task_execution_log"]);
419 assert_eq!(tables.len(), 2);
420 assert!(tables.contains("task"));
421 assert!(tables.contains("task_status"));
422 }
423
424 #[test]
425 fn test_sql_active_for() {
426 let cfg = EnvAuditConfig {
427 config: AuditConfig::production(),
428 entity_level: AuditLevel::Full,
429 sql_level: AuditLevel::Full,
430 sql_tables: Some(["task".to_string()].into_iter().collect()),
431 sink: AuditSink::Both,
432 schema_mode: SchemaMode::Verify,
433 };
434 assert!(cfg.sql_active_for("task"));
435 assert!(!cfg.sql_active_for("task_status"));
436
437 let cfg_all = EnvAuditConfig {
439 config: AuditConfig::production(),
440 entity_level: AuditLevel::Full,
441 sql_level: AuditLevel::Full,
442 sql_tables: None,
443 sink: AuditSink::Both,
444 schema_mode: SchemaMode::Verify,
445 };
446 assert!(cfg_all.sql_active_for("task"));
447 assert!(cfg_all.sql_active_for("anything"));
448
449 let cfg_silent = EnvAuditConfig {
451 config: AuditConfig::production(),
452 entity_level: AuditLevel::Full,
453 sql_level: AuditLevel::Silent,
454 sql_tables: None,
455 sink: AuditSink::Both,
456 schema_mode: SchemaMode::Verify,
457 };
458 assert!(!cfg_silent.sql_active_for("task"));
459 }
460}