1use crate::decision::Decision;
2use crate::error::Error;
3use crate::error::Result;
4use crate::executable_name::executable_path_lookup_key;
5use crate::rule::NetworkRule;
6use crate::rule::NetworkRuleProtocol;
7use crate::rule::PatternToken;
8use crate::rule::PrefixPattern;
9use crate::rule::PrefixRule;
10use crate::rule::RuleMatch;
11use crate::rule::RuleRef;
12use crate::rule::normalize_network_rule_host;
13use codex_utils_absolute_path::AbsolutePathBuf;
14use multimap::MultiMap;
15use serde::Deserialize;
16use serde::Serialize;
17use std::collections::HashMap;
18use std::sync::Arc;
19
20type HeuristicsFallback<'a> = Option<&'a dyn Fn(&[String]) -> Decision>;
21
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
23pub struct MatchOptions {
24 pub resolve_host_executables: bool,
25}
26
27#[derive(Clone, Debug)]
28pub struct Policy {
29 rules_by_program: MultiMap<String, RuleRef>,
30 network_rules: Vec<NetworkRule>,
31 host_executables_by_name: HashMap<String, Arc<[AbsolutePathBuf]>>,
32}
33
34impl Policy {
35 pub fn new(rules_by_program: MultiMap<String, RuleRef>) -> Self {
36 Self::from_parts(rules_by_program, Vec::new(), HashMap::new())
37 }
38
39 pub fn from_parts(
40 rules_by_program: MultiMap<String, RuleRef>,
41 network_rules: Vec<NetworkRule>,
42 host_executables_by_name: HashMap<String, Arc<[AbsolutePathBuf]>>,
43 ) -> Self {
44 Self {
45 rules_by_program,
46 network_rules,
47 host_executables_by_name,
48 }
49 }
50
51 pub fn empty() -> Self {
52 Self::new(MultiMap::new())
53 }
54
55 pub fn rules(&self) -> &MultiMap<String, RuleRef> {
56 &self.rules_by_program
57 }
58
59 pub fn network_rules(&self) -> &[NetworkRule] {
60 &self.network_rules
61 }
62
63 pub fn host_executables(&self) -> &HashMap<String, Arc<[AbsolutePathBuf]>> {
64 &self.host_executables_by_name
65 }
66
67 pub fn get_allowed_prefixes(&self) -> Vec<Vec<String>> {
68 let mut prefixes = Vec::new();
69
70 for (_program, rules) in self.rules_by_program.iter_all() {
71 for rule in rules {
72 let Some(prefix_rule) = rule.as_any().downcast_ref::<PrefixRule>() else {
73 continue;
74 };
75 if prefix_rule.decision != Decision::Allow {
76 continue;
77 }
78
79 let mut prefix = Vec::with_capacity(prefix_rule.pattern.rest.len() + 1);
80 prefix.push(prefix_rule.pattern.first.as_ref().to_string());
81 prefix.extend(prefix_rule.pattern.rest.iter().map(render_pattern_token));
82 prefixes.push(prefix);
83 }
84 }
85
86 prefixes.sort();
87 prefixes.dedup();
88 prefixes
89 }
90
91 pub fn add_prefix_rule(&mut self, prefix: &[String], decision: Decision) -> Result<()> {
92 let (first_token, rest) = prefix
93 .split_first()
94 .ok_or_else(|| Error::InvalidPattern("prefix cannot be empty".to_string()))?;
95
96 let rule: RuleRef = Arc::new(PrefixRule {
97 pattern: PrefixPattern {
98 first: Arc::from(first_token.as_str()),
99 rest: rest
100 .iter()
101 .map(|token| PatternToken::Single(token.clone()))
102 .collect::<Vec<_>>()
103 .into(),
104 },
105 decision,
106 justification: None,
107 });
108
109 self.rules_by_program.insert(first_token.clone(), rule);
110 Ok(())
111 }
112
113 pub fn add_network_rule(
114 &mut self,
115 host: &str,
116 protocol: NetworkRuleProtocol,
117 decision: Decision,
118 justification: Option<String>,
119 ) -> Result<()> {
120 let host = normalize_network_rule_host(host)?;
121 if let Some(raw) = justification.as_deref()
122 && raw.trim().is_empty()
123 {
124 return Err(Error::InvalidRule(
125 "justification cannot be empty".to_string(),
126 ));
127 }
128 self.network_rules.push(NetworkRule {
129 host,
130 protocol,
131 decision,
132 justification,
133 });
134 Ok(())
135 }
136
137 pub fn set_host_executable_paths(&mut self, name: String, paths: Vec<AbsolutePathBuf>) {
138 self.host_executables_by_name.insert(name, paths.into());
139 }
140
141 pub fn merge_overlay(&self, overlay: &Policy) -> Policy {
142 let mut combined_rules = self.rules_by_program.clone();
143 for (program, rules) in overlay.rules_by_program.iter_all() {
144 for rule in rules {
145 combined_rules.insert(program.clone(), rule.clone());
146 }
147 }
148
149 let mut combined_network_rules = self.network_rules.clone();
150 combined_network_rules.extend(overlay.network_rules.iter().cloned());
151
152 let mut host_executables_by_name = self.host_executables_by_name.clone();
153 host_executables_by_name.extend(
154 overlay
155 .host_executables_by_name
156 .iter()
157 .map(|(name, paths)| (name.clone(), paths.clone())),
158 );
159
160 Policy::from_parts(
161 combined_rules,
162 combined_network_rules,
163 host_executables_by_name,
164 )
165 }
166
167 pub fn compiled_network_domains(&self) -> (Vec<String>, Vec<String>) {
168 let mut allowed = Vec::new();
169 let mut denied = Vec::new();
170
171 for rule in &self.network_rules {
172 match rule.decision {
173 Decision::Allow => {
174 denied.retain(|entry| entry != &rule.host);
175 upsert_domain(&mut allowed, &rule.host);
176 }
177 Decision::Forbidden => {
178 allowed.retain(|entry| entry != &rule.host);
179 upsert_domain(&mut denied, &rule.host);
180 }
181 Decision::Prompt => {}
182 }
183 }
184
185 (allowed, denied)
186 }
187
188 pub fn check<F>(&self, cmd: &[String], heuristics_fallback: &F) -> Evaluation
189 where
190 F: Fn(&[String]) -> Decision,
191 {
192 let matched_rules = self.matches_for_command_with_options(
193 cmd,
194 Some(heuristics_fallback),
195 &MatchOptions::default(),
196 );
197 Evaluation::from_matches(matched_rules)
198 }
199
200 pub fn check_with_options<F>(
201 &self,
202 cmd: &[String],
203 heuristics_fallback: &F,
204 options: &MatchOptions,
205 ) -> Evaluation
206 where
207 F: Fn(&[String]) -> Decision,
208 {
209 let matched_rules =
210 self.matches_for_command_with_options(cmd, Some(heuristics_fallback), options);
211 Evaluation::from_matches(matched_rules)
212 }
213
214 pub fn check_multiple<Commands, F>(
216 &self,
217 commands: Commands,
218 heuristics_fallback: &F,
219 ) -> Evaluation
220 where
221 Commands: IntoIterator,
222 Commands::Item: AsRef<[String]>,
223 F: Fn(&[String]) -> Decision,
224 {
225 self.check_multiple_with_options(commands, heuristics_fallback, &MatchOptions::default())
226 }
227
228 pub fn check_multiple_with_options<Commands, F>(
229 &self,
230 commands: Commands,
231 heuristics_fallback: &F,
232 options: &MatchOptions,
233 ) -> Evaluation
234 where
235 Commands: IntoIterator,
236 Commands::Item: AsRef<[String]>,
237 F: Fn(&[String]) -> Decision,
238 {
239 let matched_rules: Vec<RuleMatch> = commands
240 .into_iter()
241 .flat_map(|command| {
242 self.matches_for_command_with_options(
243 command.as_ref(),
244 Some(heuristics_fallback),
245 options,
246 )
247 })
248 .collect();
249
250 Evaluation::from_matches(matched_rules)
251 }
252
253 pub fn matches_for_command(
261 &self,
262 cmd: &[String],
263 heuristics_fallback: HeuristicsFallback<'_>,
264 ) -> Vec<RuleMatch> {
265 self.matches_for_command_with_options(cmd, heuristics_fallback, &MatchOptions::default())
266 }
267
268 pub fn matches_for_command_with_options(
269 &self,
270 cmd: &[String],
271 heuristics_fallback: HeuristicsFallback<'_>,
272 options: &MatchOptions,
273 ) -> Vec<RuleMatch> {
274 let matched_rules = self
275 .match_exact_rules(cmd)
276 .filter(|matched_rules| !matched_rules.is_empty())
277 .or_else(|| {
278 options
279 .resolve_host_executables
280 .then(|| self.match_host_executable_rules(cmd))
281 .filter(|matched_rules| !matched_rules.is_empty())
282 })
283 .unwrap_or_default();
284
285 if matched_rules.is_empty()
286 && let Some(heuristics_fallback) = heuristics_fallback
287 {
288 vec![RuleMatch::HeuristicsRuleMatch {
289 command: cmd.to_vec(),
290 decision: heuristics_fallback(cmd),
291 }]
292 } else {
293 matched_rules
294 }
295 }
296
297 fn match_exact_rules(&self, cmd: &[String]) -> Option<Vec<RuleMatch>> {
298 let first = cmd.first()?;
299 Some(
300 self.rules_by_program
301 .get_vec(first)
302 .map(|rules| rules.iter().filter_map(|rule| rule.matches(cmd)).collect())
303 .unwrap_or_default(),
304 )
305 }
306
307 fn match_host_executable_rules(&self, cmd: &[String]) -> Vec<RuleMatch> {
308 let Some(first) = cmd.first() else {
309 return Vec::new();
310 };
311 let Ok(program) = AbsolutePathBuf::try_from(first.clone()) else {
312 return Vec::new();
313 };
314 let Some(basename) = executable_path_lookup_key(program.as_path()) else {
315 return Vec::new();
316 };
317 let Some(rules) = self.rules_by_program.get_vec(&basename) else {
318 return Vec::new();
319 };
320 if let Some(paths) = self.host_executables_by_name.get(&basename)
321 && !paths.iter().any(|path| path == &program)
322 {
323 return Vec::new();
324 }
325
326 let basename_command = std::iter::once(basename)
327 .chain(cmd.iter().skip(1).cloned())
328 .collect::<Vec<_>>();
329 rules
330 .iter()
331 .filter_map(|rule| rule.matches(&basename_command))
332 .map(|rule_match| rule_match.with_resolved_program(&program))
333 .collect()
334 }
335}
336
337fn upsert_domain(entries: &mut Vec<String>, host: &str) {
338 entries.retain(|entry| entry != host);
339 entries.push(host.to_string());
340}
341
342fn render_pattern_token(token: &PatternToken) -> String {
343 match token {
344 PatternToken::Single(value) => value.clone(),
345 PatternToken::Alts(alternatives) => format!("[{}]", alternatives.join("|")),
346 }
347}
348
349#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct Evaluation {
352 pub decision: Decision,
353 #[serde(rename = "matchedRules")]
354 pub matched_rules: Vec<RuleMatch>,
355}
356
357impl Evaluation {
358 pub fn is_match(&self) -> bool {
359 self.matched_rules
360 .iter()
361 .any(|rule_match| !matches!(rule_match, RuleMatch::HeuristicsRuleMatch { .. }))
362 }
363
364 fn from_matches(matched_rules: Vec<RuleMatch>) -> Self {
366 let decision = matched_rules.iter().map(RuleMatch::decision).max();
367 #[expect(clippy::expect_used)]
368 let decision = decision.expect("invariant failed: matched_rules must be non-empty");
369
370 Self {
371 decision,
372 matched_rules,
373 }
374 }
375}