1use crate::action::{Action, AtomicAction};
9use crate::kind::Kind;
10use crate::pattern::{GlobPattern, ShellPattern, any_match};
11use crate::policy::{Policy, PolicyError, RuleGroup};
12use crate::programs::{self, NoResolver, ProgramResolver};
13use crate::realpath::PathResolver;
14use crate::verdict::{Decision, Verdict};
15
16mod aliases;
17mod atoms;
18mod context;
19mod egress;
20mod taint;
21
22use atoms::checked_atoms;
23pub use taint::{Secret, TAINT_RULE, Taint};
24
25#[derive(Debug, Clone)]
27pub struct EvalContext {
28 pub home: String,
30 pub project: Option<String>,
35 pub real_home: Option<String>,
39 pub real_project: Option<String>,
45 pub moved_dirs: Vec<(String, String)>,
51 pub cwd: String,
53 pub case_insensitive_paths: bool,
56}
57
58#[derive(Debug)]
63pub struct CompiledPolicy<'p> {
64 policy: &'p Policy,
65 ctx: EvalContext,
66 deny: Vec<CompiledGroup<'p>>,
67 repo_ask: Vec<CompiledGroup<'p>>,
68 allow: Vec<CompiledGroup<'p>>,
69 ask: Vec<CompiledGroup<'p>>,
70 defer: Vec<CompiledGroup<'p>>,
71 protected: Vec<GlobPattern>,
73}
74
75#[derive(Debug)]
76struct CompiledGroup<'p> {
77 group: &'p RuleGroup,
78 shell: Vec<ShellPattern>,
79 globs: [Vec<GlobPattern>; Kind::ALL.len()],
81 bare_hosts: Option<[Vec<GlobPattern>; Kind::ALL.len()]>,
87}
88
89fn names_port(pattern: &str) -> bool {
92 !pattern.starts_with('!') && crate::host::port_pattern(pattern).is_some()
93}
94
95impl<'p> CompiledPolicy<'p> {
96 pub fn compile(policy: &'p Policy, ctx: &EvalContext) -> Result<Self, PolicyError> {
100 ctx.validate_moved_dirs()?;
101 let compile_list = |groups: &'p [RuleGroup], allow: bool| {
102 groups
103 .iter()
104 .map(|g| CompiledGroup::compile(g, ctx, allow))
105 .collect::<Result<Vec<_>, _>>()
106 };
107 Ok(Self {
108 policy,
109 ctx: ctx.clone(),
110 deny: compile_list(&policy.deny, false)?,
111 repo_ask: compile_list(&policy.repo_ask, false)?,
112 allow: compile_list(&policy.allow, true)?,
113 ask: compile_list(&policy.ask, false)?,
114 defer: compile_list(&policy.defer, false)?,
115 protected: taint::compile_protected(policy, ctx)?,
116 })
117 }
118
119 #[must_use]
121 pub fn policy(&self) -> &'p Policy {
122 self.policy
123 }
124
125 #[must_use]
127 pub fn context(&self) -> &EvalContext {
128 &self.ctx
129 }
130
131 #[must_use]
138 pub fn evaluate_atomic(&self, action: &AtomicAction) -> Option<Decision> {
139 if let Some(hit) = self.matched_rules(action) {
140 return Some(hit);
141 }
142 if matches!(action, AtomicAction::Pipeline { .. }) {
143 return None;
144 }
145 let (verdict, rule_id) = self.policy.defaults.for_atom(action);
146 Some(Decision::single(
147 verdict,
148 &rule_id,
149 format!("no rule matched {}", action.describe()),
150 ))
151 }
152
153 fn matched_rules(&self, action: &AtomicAction) -> Option<Decision> {
156 for (groups, verdict) in [
157 (&self.deny, Verdict::Deny),
158 (&self.repo_ask, Verdict::Ask),
159 (&self.allow, Verdict::Allow),
160 (&self.ask, Verdict::Ask),
161 (&self.defer, Verdict::Defer),
162 ] {
163 let mut hit = Decision::new(verdict);
164 for g in groups {
165 if g.matches(action) {
166 let why = g.group.reason.as_ref().map_or_else(
167 || action.describe(),
168 |r| format!("{r}: {}", action.describe()),
169 );
170 hit.push(&g.group.id, why);
171 }
172 }
173 if !hit.rules.is_empty() {
174 return Some(hit);
175 }
176 }
177 None
178 }
179}
180
181impl<'p> CompiledGroup<'p> {
182 fn compile(group: &'p RuleGroup, ctx: &EvalContext, allow: bool) -> Result<Self, PolicyError> {
184 let shell = group
185 .shell
186 .iter()
187 .map(|p| ShellPattern::compile(p))
188 .collect::<Result<_, _>>()?;
189 let mut globs: [Vec<GlobPattern>; Kind::ALL.len()] = Default::default();
190 let mut bare: [Vec<GlobPattern>; Kind::ALL.len()] = Default::default();
191 for kind in Kind::ALL.into_iter().filter(|k| *k != Kind::Shell) {
192 let case_insensitive = match kind {
193 Kind::FsRead | Kind::FsWrite => ctx.case_insensitive_paths,
194 Kind::Net | Kind::Fetch => true,
195 _ => false,
196 };
197 let compile = |patterns: &mut dyn Iterator<Item = &String>| {
198 patterns
199 .flat_map(|p| ctx.spellings(p))
200 .map(|p| GlobPattern::compile(&p, case_insensitive))
201 .collect::<Result<Vec<_>, _>>()
202 };
203 let patterns = group.patterns(kind);
204 globs[kind as usize] = compile(&mut patterns.iter())?;
205 if matches!(kind, Kind::Net | Kind::Fetch) {
206 bare[kind as usize] = compile(&mut patterns.iter().filter(|p| !names_port(p)))?;
207 }
208 }
209 Ok(Self {
210 group,
211 shell,
212 globs,
213 bare_hosts: allow.then_some(bare),
214 })
215 }
216
217 fn matches(&self, action: &AtomicAction) -> bool {
218 match (action, action.subject()) {
219 (AtomicAction::Shell { argv } | AtomicAction::Pipeline { argv }, _) => {
220 any_match(&self.shell, argv.as_slice())
221 }
222 (_, Some(subject)) => {
223 let globs = match &self.bare_hosts {
224 Some(bare)
225 if matches!(
226 action,
227 AtomicAction::Net { .. } | AtomicAction::Fetch { .. }
228 ) && crate::host::port_pattern(subject).is_none() =>
229 {
230 bare
231 }
232 _ => &self.globs,
233 };
234 action
235 .kind()
236 .rule_kinds()
237 .iter()
238 .any(|k| any_match(&globs[*k as usize], subject))
239 }
240 (_, None) => false,
241 }
242 }
243}
244
245impl CompiledPolicy<'_> {
246 #[must_use]
251 pub fn decide(&self, action: &Action) -> Decision {
252 self.decide_with(action, &NoResolver, &NoResolver)
253 }
254
255 #[must_use]
261 pub fn atoms(
262 &self,
263 action: &Action,
264 paths: &dyn PathResolver,
265 ) -> (Vec<AtomicAction>, Option<String>) {
266 let (atoms, unparsed) = self.all_atoms(action, paths);
267 (atoms, unparsed.map(|u| u.reason))
268 }
269
270 fn all_atoms(
273 &self,
274 action: &Action,
275 paths: &dyn PathResolver,
276 ) -> (Vec<AtomicAction>, Option<crate::shell::Unparsed>) {
277 let (mut atoms, unparsed) = checked_atoms(action, &self.ctx, paths);
278 let egress = egress::atoms(action, &atoms, &self.policy.secrets);
279 atoms.extend(egress);
280 (atoms, unparsed)
281 }
282
283 #[must_use]
287 pub fn pushes(&self, action: &Action) -> Vec<crate::shell::push::Push> {
288 let Action::Shell { command } = action.shown() else {
289 return Vec::new();
290 };
291 crate::shell::push::pushes(
292 command,
293 &crate::shell::ShellContext {
294 home: &self.ctx.home,
295 cwd: &[Some(self.ctx.cwd.clone())],
296 },
297 )
298 }
299
300 #[must_use]
304 pub fn decide_with(
305 &self,
306 action: &Action,
307 resolver: &dyn ProgramResolver,
308 paths: &dyn PathResolver,
309 ) -> Decision {
310 let (atoms, unparsed) = self.all_atoms(action, paths);
311 let pins = atoms.iter().filter_map(|atom| match atom {
312 AtomicAction::Shell { argv } => {
313 programs::check(&argv[0], &self.policy.executables, resolver)
314 }
315 _ => None,
316 });
317 let aliased = aliases::aliases(&atoms, &self.ctx, resolver, paths)
320 .into_iter()
321 .filter_map(|atom| self.matched_rules(&atom))
322 .filter(|hit| matches!(hit.verdict, Verdict::Deny | Verdict::Ask));
323 let verdict = self.policy.defaults.unclassified();
328 let unparsed = unparsed.map(|u| {
329 let verdict = if u.asks { Verdict::Ask } else { verdict };
330 unparseable(
331 verdict,
332 format!("could not parse action safely: {}", u.reason),
333 )
334 });
335 let unchecked = match action.shown() {
336 Action::McpTool {
337 unchecked: Some(limit),
338 ..
339 } => Some(unparseable(
340 verdict,
341 format!("mcp arguments not fully checked: {limit}"),
342 )),
343 _ => None,
344 };
345 let partial = match action {
347 Action::Unchecked { reason, .. } => Some(Decision::single(
348 Verdict::Ask,
349 crate::action::UNCHECKED_RULE,
350 reason.clone(),
351 )),
352 _ => None,
353 };
354 atoms
355 .iter()
356 .filter_map(|atom| self.evaluate_atomic(atom))
357 .chain(aliased)
358 .chain(pins)
359 .chain(unparsed)
360 .chain(unchecked)
361 .chain(partial)
362 .reduce(|mut acc, next| {
363 acc.merge(next);
364 acc
365 })
366 .unwrap_or_else(|| unparseable(verdict, "action has no evaluable parts".to_owned()))
367 }
368}
369
370fn unparseable(verdict: Verdict, reason: String) -> Decision {
372 Decision::single(verdict, "unparseable", reason)
373}
374
375pub fn evaluate(
380 policy: &Policy,
381 ctx: &EvalContext,
382 action: &Action,
383) -> Result<Decision, PolicyError> {
384 Ok(CompiledPolicy::compile(policy, ctx)?.decide(action))
385}
386
387#[cfg(test)]
388mod tests;