Skip to main content

openmoat_core/ir/
check.rs

1//! Evaluating the IR on atomic actions: the reference semantics every OS
2//! backend must reproduce, and what the consistency tests compare with the engine.
3
4use super::{Access, Effect, Enforcement};
5use crate::action::AtomicAction;
6use crate::kind::Kind;
7use crate::pattern::{GlobPattern, any_match};
8use crate::policy::PolicyError;
9
10/// An [`Enforcement`] with its globs compiled.
11#[derive(Debug)]
12pub struct Checker {
13    /// The read allowances, every one an `fs.read` [`super::Allowance`].
14    read_allowances: Vec<GlobPattern>,
15    read: CompiledAccess,
16    write: CompiledAccess,
17    net: CompiledAccess,
18    fetch: CompiledAccess,
19}
20
21#[derive(Debug)]
22struct CompiledAccess {
23    default: Effect,
24    deny: Vec<Vec<GlobPattern>>,
25    allow: Vec<Vec<GlobPattern>>,
26}
27
28impl Enforcement {
29    /// Compile the IR's globs for [`Checker::check`].
30    pub fn checker(&self) -> Result<Checker, PolicyError> {
31        let paths = self.fs.case_insensitive;
32        Ok(Checker {
33            read_allowances: self
34                .allowances
35                .iter()
36                .filter(|a| a.kind == Kind::FsRead)
37                .flat_map(|a| &a.patterns)
38                .map(|p| GlobPattern::compile(p, paths))
39                .collect::<Result<_, _>>()?,
40            read: CompiledAccess::compile(&self.fs.read, paths)?,
41            write: CompiledAccess::compile(&self.fs.write, paths)?,
42            net: CompiledAccess::compile(&self.egress.net, true)?,
43            fetch: CompiledAccess::compile(&self.egress.fetch, true)?,
44        })
45    }
46}
47
48impl Checker {
49    /// What an OS layer does with `atom`: `None` for a decide-only kind.
50    #[must_use]
51    pub fn check(&self, atom: &AtomicAction) -> Option<Effect> {
52        let access = match atom.kind() {
53            Kind::FsRead => &self.read,
54            Kind::FsWrite => &self.write,
55            Kind::Net => &self.net,
56            Kind::Fetch => &self.fetch,
57            Kind::Shell | Kind::EnvRead | Kind::EnvSet | Kind::Mcp => return None,
58        };
59        Some(access.effect(atom.subject()?))
60    }
61
62    /// What an OS layer does with `atom` once the [`super::Allowance`]s apply:
63    /// a deny rule still denies, then an allowance allows, then [`Self::check`].
64    #[must_use]
65    pub fn check_os(&self, atom: &AtomicAction) -> Option<Effect> {
66        let effect = self.check(atom)?;
67        let allowed = atom.kind() == Kind::FsRead
68            && !self.read.denies(atom.subject()?)
69            && any_match(&self.read_allowances, atom.subject()?);
70        Some(if allowed { Effect::Allow } else { effect })
71    }
72}
73
74impl CompiledAccess {
75    fn compile(access: &Access, case_insensitive: bool) -> Result<Self, PolicyError> {
76        let compile = |rules: &[super::Rule]| {
77            rules
78                .iter()
79                .map(|rule| {
80                    rule.patterns
81                        .iter()
82                        .map(|p| GlobPattern::compile(p, case_insensitive))
83                        .collect::<Result<Vec<_>, _>>()
84                })
85                .collect::<Result<Vec<_>, _>>()
86        };
87        Ok(Self {
88            default: access.default,
89            deny: compile(&access.deny)?,
90            allow: compile(&access.allow)?,
91        })
92    }
93
94    fn denies(&self, subject: &str) -> bool {
95        self.deny.iter().any(|r| any_match(r, subject))
96    }
97
98    fn effect(&self, subject: &str) -> Effect {
99        let hit = |rules: &[Vec<GlobPattern>]| rules.iter().any(|r| any_match(r, subject));
100        if self.denies(subject) {
101            Effect::Deny
102        } else if hit(&self.allow) {
103            Effect::Allow
104        } else {
105            self.default
106        }
107    }
108}