Skip to main content

codex_execpolicy/
parser.rs

1use codex_utils_absolute_path::AbsolutePathBuf;
2use multimap::MultiMap;
3use starlark::any::ProvidesStaticType;
4use starlark::codemap::FileSpan;
5use starlark::environment::GlobalsBuilder;
6use starlark::environment::Module;
7use starlark::eval::Evaluator;
8use starlark::starlark_module;
9use starlark::syntax::AstModule;
10use starlark::syntax::Dialect;
11use starlark::values::Value;
12use starlark::values::list::ListRef;
13use starlark::values::list::UnpackList;
14use starlark::values::none::NoneType;
15use std::cell::RefCell;
16use std::cell::RefMut;
17use std::collections::HashMap;
18use std::path::Path;
19use std::sync::Arc;
20
21use crate::decision::Decision;
22use crate::error::Error;
23use crate::error::ErrorLocation;
24use crate::error::Result;
25use crate::error::TextPosition;
26use crate::error::TextRange;
27use crate::executable_name::executable_lookup_key;
28use crate::executable_name::executable_path_lookup_key;
29use crate::rule::NetworkRule;
30use crate::rule::NetworkRuleProtocol;
31use crate::rule::PatternToken;
32use crate::rule::PrefixPattern;
33use crate::rule::PrefixRule;
34use crate::rule::RuleRef;
35use crate::rule::validate_match_examples;
36use crate::rule::validate_not_match_examples;
37
38pub struct PolicyParser {
39    builder: RefCell<PolicyBuilder>,
40}
41
42impl Default for PolicyParser {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl PolicyParser {
49    pub fn new() -> Self {
50        Self {
51            builder: RefCell::new(PolicyBuilder::new()),
52        }
53    }
54
55    /// Parses a policy, tagging parser errors with `policy_identifier` so failures include the
56    /// identifier alongside line numbers.
57    pub fn parse(&mut self, policy_identifier: &str, policy_file_contents: &str) -> Result<()> {
58        let pending_validation_count = self.builder.borrow().pending_example_validations.len();
59        let mut dialect = Dialect::Extended.clone();
60        dialect.enable_f_strings = true;
61        let ast = AstModule::parse(
62            policy_identifier,
63            policy_file_contents.to_string(),
64            &dialect,
65        )
66        .map_err(Error::Starlark)?;
67        let globals = GlobalsBuilder::standard().with(policy_builtins).build();
68        Module::with_temp_heap(|module| {
69            let mut eval = Evaluator::new(&module);
70            eval.extra = Some(&self.builder);
71            eval.eval_module(ast, &globals)
72                .map(|_| ())
73                .map_err(Error::Starlark)
74        })?;
75        self.builder
76            .borrow()
77            .validate_pending_examples_from(pending_validation_count)?;
78        Ok(())
79    }
80
81    pub fn build(self) -> crate::policy::Policy {
82        self.builder.into_inner().build()
83    }
84}
85
86#[derive(Debug, ProvidesStaticType)]
87struct PolicyBuilder {
88    rules_by_program: MultiMap<String, RuleRef>,
89    network_rules: Vec<NetworkRule>,
90    host_executables_by_name: HashMap<String, Arc<[AbsolutePathBuf]>>,
91    pending_example_validations: Vec<PendingExampleValidation>,
92}
93
94impl PolicyBuilder {
95    fn new() -> Self {
96        Self {
97            rules_by_program: MultiMap::new(),
98            network_rules: Vec::new(),
99            host_executables_by_name: HashMap::new(),
100            pending_example_validations: Vec::new(),
101        }
102    }
103
104    fn add_rule(&mut self, rule: RuleRef) {
105        self.rules_by_program
106            .insert(rule.program().to_string(), rule);
107    }
108
109    fn add_network_rule(&mut self, rule: NetworkRule) {
110        self.network_rules.push(rule);
111    }
112
113    fn add_host_executable(&mut self, name: String, paths: Vec<AbsolutePathBuf>) {
114        self.host_executables_by_name.insert(name, paths.into());
115    }
116
117    fn add_pending_example_validation(
118        &mut self,
119        rules: Vec<RuleRef>,
120        matches: Vec<Vec<String>>,
121        not_matches: Vec<Vec<String>>,
122        location: Option<ErrorLocation>,
123    ) {
124        self.pending_example_validations
125            .push(PendingExampleValidation {
126                rules,
127                matches,
128                not_matches,
129                location,
130            });
131    }
132
133    fn validate_pending_examples_from(&self, start: usize) -> Result<()> {
134        for validation in &self.pending_example_validations[start..] {
135            let mut rules_by_program = MultiMap::new();
136            for rule in &validation.rules {
137                rules_by_program.insert(rule.program().to_string(), rule.clone());
138            }
139
140            let policy = crate::policy::Policy::from_parts(
141                rules_by_program,
142                Vec::new(),
143                self.host_executables_by_name.clone(),
144            );
145            validate_not_match_examples(&policy, &validation.rules, &validation.not_matches)
146                .map_err(|error| attach_validation_location(error, validation.location.clone()))?;
147            validate_match_examples(&policy, &validation.rules, &validation.matches)
148                .map_err(|error| attach_validation_location(error, validation.location.clone()))?;
149        }
150
151        Ok(())
152    }
153
154    fn build(self) -> crate::policy::Policy {
155        crate::policy::Policy::from_parts(
156            self.rules_by_program,
157            self.network_rules,
158            self.host_executables_by_name,
159        )
160    }
161}
162
163#[derive(Debug)]
164struct PendingExampleValidation {
165    rules: Vec<RuleRef>,
166    matches: Vec<Vec<String>>,
167    not_matches: Vec<Vec<String>>,
168    location: Option<ErrorLocation>,
169}
170
171fn parse_pattern<'v>(pattern: UnpackList<Value<'v>>) -> Result<Vec<PatternToken>> {
172    let tokens: Vec<PatternToken> = pattern
173        .items
174        .into_iter()
175        .map(parse_pattern_token)
176        .collect::<Result<_>>()?;
177    if tokens.is_empty() {
178        Err(Error::InvalidPattern("pattern cannot be empty".to_string()))
179    } else {
180        Ok(tokens)
181    }
182}
183
184fn parse_pattern_token<'v>(value: Value<'v>) -> Result<PatternToken> {
185    if let Some(s) = value.unpack_str() {
186        Ok(PatternToken::Single(s.to_string()))
187    } else if let Some(list) = ListRef::from_value(value) {
188        let tokens: Vec<String> = list
189            .content()
190            .iter()
191            .map(|value| {
192                value
193                    .unpack_str()
194                    .ok_or_else(|| {
195                        Error::InvalidPattern(format!(
196                            "pattern alternative must be a string (got {})",
197                            value.get_type()
198                        ))
199                    })
200                    .map(str::to_string)
201            })
202            .collect::<Result<_>>()?;
203
204        match tokens.as_slice() {
205            [] => Err(Error::InvalidPattern(
206                "pattern alternatives cannot be empty".to_string(),
207            )),
208            [single] => Ok(PatternToken::Single(single.clone())),
209            _ => Ok(PatternToken::Alts(tokens)),
210        }
211    } else {
212        Err(Error::InvalidPattern(format!(
213            "pattern element must be a string or list of strings (got {})",
214            value.get_type()
215        )))
216    }
217}
218
219fn parse_examples<'v>(examples: UnpackList<Value<'v>>) -> Result<Vec<Vec<String>>> {
220    examples.items.into_iter().map(parse_example).collect()
221}
222
223fn parse_literal_absolute_path(raw: &str) -> Result<AbsolutePathBuf> {
224    if !Path::new(raw).is_absolute() {
225        return Err(Error::InvalidRule(format!(
226            "host_executable paths must be absolute (got {raw})"
227        )));
228    }
229
230    AbsolutePathBuf::try_from(raw.to_string())
231        .map_err(|error| Error::InvalidRule(format!("invalid absolute path `{raw}`: {error}")))
232}
233
234fn validate_host_executable_name(name: &str) -> Result<()> {
235    if name.is_empty() {
236        return Err(Error::InvalidRule(
237            "host_executable name cannot be empty".to_string(),
238        ));
239    }
240
241    let path = Path::new(name);
242    if path.components().count() != 1
243        || path.file_name().and_then(|value| value.to_str()) != Some(name)
244    {
245        return Err(Error::InvalidRule(format!(
246            "host_executable name must be a bare executable name (got {name})"
247        )));
248    }
249
250    Ok(())
251}
252
253fn parse_network_rule_decision(raw: &str) -> Result<Decision> {
254    match raw {
255        "deny" => Ok(Decision::Forbidden),
256        other => Decision::parse(other),
257    }
258}
259
260fn error_location_from_file_span(span: FileSpan) -> ErrorLocation {
261    let resolved = span.resolve_span();
262    ErrorLocation {
263        path: span.filename().to_string(),
264        range: TextRange {
265            start: TextPosition {
266                line: resolved.begin.line + 1,
267                column: resolved.begin.column + 1,
268            },
269            end: TextPosition {
270                line: resolved.end.line + 1,
271                column: resolved.end.column + 1,
272            },
273        },
274    }
275}
276
277fn attach_validation_location(error: Error, location: Option<ErrorLocation>) -> Error {
278    match location {
279        Some(location) => error.with_location(location),
280        None => error,
281    }
282}
283
284fn parse_example<'v>(value: Value<'v>) -> Result<Vec<String>> {
285    if let Some(raw) = value.unpack_str() {
286        parse_string_example(raw)
287    } else if let Some(list) = ListRef::from_value(value) {
288        parse_list_example(list)
289    } else {
290        Err(Error::InvalidExample(format!(
291            "example must be a string or list of strings (got {})",
292            value.get_type()
293        )))
294    }
295}
296
297fn parse_string_example(raw: &str) -> Result<Vec<String>> {
298    let tokens = shlex::split(raw).ok_or_else(|| {
299        Error::InvalidExample("example string has invalid shell syntax".to_string())
300    })?;
301
302    if tokens.is_empty() {
303        Err(Error::InvalidExample(
304            "example cannot be an empty string".to_string(),
305        ))
306    } else {
307        Ok(tokens)
308    }
309}
310
311fn parse_list_example(list: &ListRef) -> Result<Vec<String>> {
312    let tokens: Vec<String> = list
313        .content()
314        .iter()
315        .map(|value| {
316            value
317                .unpack_str()
318                .ok_or_else(|| {
319                    Error::InvalidExample(format!(
320                        "example tokens must be strings (got {})",
321                        value.get_type()
322                    ))
323                })
324                .map(str::to_string)
325        })
326        .collect::<Result<_>>()?;
327
328    if tokens.is_empty() {
329        Err(Error::InvalidExample(
330            "example cannot be an empty list".to_string(),
331        ))
332    } else {
333        Ok(tokens)
334    }
335}
336
337fn policy_builder<'v, 'a>(eval: &Evaluator<'v, 'a, '_>) -> RefMut<'a, PolicyBuilder> {
338    #[expect(clippy::expect_used)]
339    eval.extra
340        .as_ref()
341        .expect("policy_builder requires Evaluator.extra to be populated")
342        .downcast_ref::<RefCell<PolicyBuilder>>()
343        .expect("Evaluator.extra must contain a PolicyBuilder")
344        .borrow_mut()
345}
346
347#[starlark_module]
348fn policy_builtins(builder: &mut GlobalsBuilder) {
349    fn prefix_rule<'v>(
350        pattern: UnpackList<Value<'v>>,
351        decision: Option<&'v str>,
352        r#match: Option<UnpackList<Value<'v>>>,
353        not_match: Option<UnpackList<Value<'v>>>,
354        justification: Option<&'v str>,
355        eval: &mut Evaluator<'v, '_, '_>,
356    ) -> anyhow::Result<NoneType> {
357        let decision = match decision {
358            Some(raw) => Decision::parse(raw)?,
359            None => Decision::Allow,
360        };
361
362        let justification = match justification {
363            Some(raw) if raw.trim().is_empty() => {
364                return Err(Error::InvalidRule("justification cannot be empty".to_string()).into());
365            }
366            Some(raw) => Some(raw.to_string()),
367            None => None,
368        };
369
370        let pattern_tokens = parse_pattern(pattern)?;
371
372        let matches: Vec<Vec<String>> =
373            r#match.map(parse_examples).transpose()?.unwrap_or_default();
374        let not_matches: Vec<Vec<String>> = not_match
375            .map(parse_examples)
376            .transpose()?
377            .unwrap_or_default();
378        let location = eval
379            .call_stack_top_location()
380            .map(error_location_from_file_span);
381
382        let mut builder = policy_builder(eval);
383
384        let (first_token, remaining_tokens) = pattern_tokens
385            .split_first()
386            .ok_or_else(|| Error::InvalidPattern("pattern cannot be empty".to_string()))?;
387
388        let rest: Arc<[PatternToken]> = remaining_tokens.to_vec().into();
389
390        let rules: Vec<RuleRef> = first_token
391            .alternatives()
392            .iter()
393            .map(|head| {
394                Arc::new(PrefixRule {
395                    pattern: PrefixPattern {
396                        first: Arc::from(head.as_str()),
397                        rest: rest.clone(),
398                    },
399                    decision,
400                    justification: justification.clone(),
401                }) as RuleRef
402            })
403            .collect();
404
405        builder.add_pending_example_validation(rules.clone(), matches, not_matches, location);
406        rules.into_iter().for_each(|rule| builder.add_rule(rule));
407        Ok(NoneType)
408    }
409
410    fn network_rule<'v>(
411        host: &'v str,
412        protocol: &'v str,
413        decision: &'v str,
414        justification: Option<&'v str>,
415        eval: &mut Evaluator<'v, '_, '_>,
416    ) -> anyhow::Result<NoneType> {
417        let protocol = NetworkRuleProtocol::parse(protocol)?;
418        let decision = parse_network_rule_decision(decision)?;
419        let justification = match justification {
420            Some(raw) if raw.trim().is_empty() => {
421                return Err(Error::InvalidRule("justification cannot be empty".to_string()).into());
422            }
423            Some(raw) => Some(raw.to_string()),
424            None => None,
425        };
426
427        let mut builder = policy_builder(eval);
428        builder.add_network_rule(NetworkRule {
429            host: crate::rule::normalize_network_rule_host(host)?,
430            protocol,
431            decision,
432            justification,
433        });
434        Ok(NoneType)
435    }
436
437    fn host_executable<'v>(
438        name: &'v str,
439        paths: UnpackList<Value<'v>>,
440        eval: &mut Evaluator<'v, '_, '_>,
441    ) -> anyhow::Result<NoneType> {
442        validate_host_executable_name(name)?;
443
444        let mut parsed_paths = Vec::new();
445        for value in paths.items {
446            let raw = value.unpack_str().ok_or_else(|| {
447                Error::InvalidRule(format!(
448                    "host_executable paths must be strings (got {})",
449                    value.get_type()
450                ))
451            })?;
452            let path = parse_literal_absolute_path(raw)?;
453            let Some(path_name) = executable_path_lookup_key(path.as_path()) else {
454                return Err(Error::InvalidRule(format!(
455                    "host_executable path `{raw}` must have basename `{name}`"
456                ))
457                .into());
458            };
459            if path_name != executable_lookup_key(name) {
460                return Err(Error::InvalidRule(format!(
461                    "host_executable path `{raw}` must have basename `{name}`"
462                ))
463                .into());
464            }
465            if !parsed_paths.iter().any(|existing| existing == &path) {
466                parsed_paths.push(path);
467            }
468        }
469
470        policy_builder(eval).add_host_executable(executable_lookup_key(name), parsed_paths);
471        Ok(NoneType)
472    }
473}