leviath_cli/daemon/
gate_rules.rs1use std::path::Path;
7use std::sync::Arc;
8
9use leviath_runtime::taint::ScriptRuleChecker;
10
11pub fn build_gate_script_checker(rules_dir: &Path) -> Arc<ScriptRuleChecker> {
18 let scripts: Vec<(String, String)> = std::fs::read_dir(rules_dir)
19 .ok()
20 .into_iter()
21 .flatten() .flatten() .filter_map(|entry| {
24 let path = entry.path();
25 if path.extension().and_then(|e| e.to_str()) != Some("rhai") {
26 return None;
27 }
28 let name = path
29 .file_stem()
30 .and_then(|s| s.to_str())
31 .unwrap_or("rule")
32 .to_string();
33 std::fs::read_to_string(&path)
34 .ok()
35 .map(|source| (name, source))
36 })
37 .collect();
38 if scripts.is_empty() {
39 return Arc::new(|_tool, _target, _taint| None);
40 }
41
42 let engine = leviath_scripting::ScriptEngine::new();
43 Arc::new(
44 move |tool: &str,
45 target: Option<&str>,
46 taint: leviath_core::TaintLevel|
47 -> Option<String> {
48 scripts.iter().find_map(|(name, source)| {
49 engine
50 .check_gate_rule(source, tool, target, taint.as_str())
51 .unwrap_or(false)
52 .then(|| name.clone())
53 })
54 },
55 )
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use leviath_core::TaintLevel;
62
63 fn write_rule(dir: &Path, name: &str, body: &str) {
64 std::fs::write(dir.join(name), body).unwrap();
65 }
66
67 #[test]
68 fn missing_or_empty_dir_yields_a_noop_checker() {
69 let dir = tempfile::tempdir().unwrap();
71 let noop = build_gate_script_checker(&dir.path().join("nope"));
72 assert_eq!(noop("shell", None, TaintLevel::Public), None);
73 write_rule(dir.path(), "notes.txt", "ignored");
75 let noop2 = build_gate_script_checker(dir.path());
76 assert_eq!(noop2("shell", None, TaintLevel::Public), None);
77 }
78
79 #[test]
80 fn a_matching_rule_allows_and_names_itself() {
81 let dir = tempfile::tempdir().unwrap();
82 write_rule(dir.path(), "company.rhai", r#"context.tool == "shell""#);
84 let checker = build_gate_script_checker(dir.path());
85
86 assert_eq!(
88 checker("shell", None, TaintLevel::Internal),
89 Some("company".to_string())
90 );
91 assert_eq!(checker("read_file", None, TaintLevel::Internal), None);
93 }
94
95 #[test]
96 fn a_rule_can_key_on_target_and_taint() {
97 let dir = tempfile::tempdir().unwrap();
98 write_rule(
99 dir.path(),
100 "internal_only.rhai",
101 r#"context.taint_level == "internal" && context.target == "ops@corp""#,
102 );
103 let checker = build_gate_script_checker(dir.path());
104 assert!(checker("send_email", Some("ops@corp"), TaintLevel::Internal).is_some());
105 assert!(checker("send_email", Some("ops@corp"), TaintLevel::Private).is_none());
106 }
107
108 #[test]
109 fn a_script_that_errors_is_treated_as_no_match() {
110 let dir = tempfile::tempdir().unwrap();
111 write_rule(dir.path(), "broken.rhai", "this is not valid rhai @@@");
113 let checker = build_gate_script_checker(dir.path());
114 assert_eq!(checker("shell", None, TaintLevel::Public), None);
115 }
116}