agcodex_execpolicy/
policy_parser.rs1#![allow(clippy::needless_lifetimes)]
2
3use crate::Opt;
4use crate::Policy;
5use crate::ProgramSpec;
6use crate::arg_matcher::ArgMatcher;
7use crate::opt::OptMeta;
8use log::info;
9use multimap::MultiMap;
10use regex_lite::Regex;
11use starlark::any::ProvidesStaticType;
12use starlark::environment::GlobalsBuilder;
13use starlark::environment::LibraryExtension;
14use starlark::environment::Module;
15use starlark::eval::Evaluator;
16use starlark::syntax::AstModule;
17use starlark::syntax::Dialect;
18use starlark::values::Heap;
19use starlark::values::list::UnpackList;
20use starlark::values::none::NoneType;
21use std::cell::RefCell;
22use std::collections::HashMap;
23
24pub struct PolicyParser {
25 policy_source: String,
26 unparsed_policy: String,
27}
28
29impl PolicyParser {
30 pub fn new(policy_source: &str, unparsed_policy: &str) -> Self {
31 Self {
32 policy_source: policy_source.to_string(),
33 unparsed_policy: unparsed_policy.to_string(),
34 }
35 }
36
37 pub fn parse(&self) -> starlark::Result<Policy> {
38 let mut dialect = Dialect::Extended.clone();
39 dialect.enable_f_strings = true;
40 let ast = AstModule::parse(&self.policy_source, self.unparsed_policy.clone(), &dialect)?;
41 let globals = GlobalsBuilder::extended_by(&[LibraryExtension::Typing])
42 .with(policy_builtins)
43 .build();
44 let module = Module::new();
45
46 let heap = Heap::new();
47
48 module.set("ARG_OPAQUE_VALUE", heap.alloc(ArgMatcher::OpaqueNonFile));
49 module.set("ARG_RFILE", heap.alloc(ArgMatcher::ReadableFile));
50 module.set("ARG_WFILE", heap.alloc(ArgMatcher::WriteableFile));
51 module.set("ARG_RFILES", heap.alloc(ArgMatcher::ReadableFiles));
52 module.set(
53 "ARG_RFILES_OR_CWD",
54 heap.alloc(ArgMatcher::ReadableFilesOrCwd),
55 );
56 module.set("ARG_POS_INT", heap.alloc(ArgMatcher::PositiveInteger));
57 module.set("ARG_SED_COMMAND", heap.alloc(ArgMatcher::SedCommand));
58 module.set(
59 "ARG_UNVERIFIED_VARARGS",
60 heap.alloc(ArgMatcher::UnverifiedVarargs),
61 );
62
63 let policy_builder = PolicyBuilder::new();
64 {
65 let mut eval = Evaluator::new(&module);
66 eval.extra = Some(&policy_builder);
67 eval.eval_module(ast, &globals)?;
68 }
69 let policy = policy_builder.build();
70 policy.map_err(|e| starlark::Error::new_kind(starlark::ErrorKind::Other(e.into())))
71 }
72}
73
74#[derive(Debug)]
75pub struct ForbiddenProgramRegex {
76 pub regex: regex_lite::Regex,
77 pub reason: String,
78}
79
80#[derive(Debug, ProvidesStaticType)]
81struct PolicyBuilder {
82 programs: RefCell<MultiMap<String, ProgramSpec>>,
83 forbidden_program_regexes: RefCell<Vec<ForbiddenProgramRegex>>,
84 forbidden_substrings: RefCell<Vec<String>>,
85}
86
87impl PolicyBuilder {
88 fn new() -> Self {
89 Self {
90 programs: RefCell::new(MultiMap::new()),
91 forbidden_program_regexes: RefCell::new(Vec::new()),
92 forbidden_substrings: RefCell::new(Vec::new()),
93 }
94 }
95
96 fn build(self) -> Result<Policy, regex_lite::Error> {
97 let programs = self.programs.into_inner();
98 let forbidden_program_regexes = self.forbidden_program_regexes.into_inner();
99 let forbidden_substrings = self.forbidden_substrings.into_inner();
100 Policy::new(programs, forbidden_program_regexes, forbidden_substrings)
101 }
102
103 fn add_program_spec(&self, program_spec: ProgramSpec) {
104 info!("adding program spec: {program_spec:?}");
105 let name = program_spec.program.clone();
106 let mut programs = self.programs.borrow_mut();
107 programs.insert(name.clone(), program_spec);
108 }
109
110 fn add_forbidden_substrings(&self, substrings: &[String]) {
111 let mut forbidden_substrings = self.forbidden_substrings.borrow_mut();
112 forbidden_substrings.extend_from_slice(substrings);
113 }
114
115 fn add_forbidden_program_regex(&self, regex: Regex, reason: String) {
116 let mut forbidden_program_regexes = self.forbidden_program_regexes.borrow_mut();
117 forbidden_program_regexes.push(ForbiddenProgramRegex { regex, reason });
118 }
119}
120
121#[starlark_module]
122fn policy_builtins(builder: &mut GlobalsBuilder) {
123 fn define_program<'v>(
124 program: String,
125 system_path: Option<UnpackList<String>>,
126 option_bundling: Option<bool>,
127 combined_format: Option<bool>,
128 options: Option<UnpackList<Opt>>,
129 args: Option<UnpackList<ArgMatcher>>,
130 forbidden: Option<String>,
131 should_match: Option<UnpackList<UnpackList<String>>>,
132 should_not_match: Option<UnpackList<UnpackList<String>>>,
133 eval: &mut Evaluator,
134 ) -> anyhow::Result<NoneType> {
135 let option_bundling = option_bundling.unwrap_or(false);
136 let system_path = system_path.map_or_else(Vec::new, |v| v.items.to_vec());
137 let combined_format = combined_format.unwrap_or(false);
138 let options = options.map_or_else(Vec::new, |v| v.items.to_vec());
139 let args = args.map_or_else(Vec::new, |v| v.items.to_vec());
140
141 let mut allowed_options = HashMap::<String, Opt>::new();
142 for opt in options {
143 let name = opt.name().to_string();
144 if allowed_options
145 .insert(opt.name().to_string(), opt)
146 .is_some()
147 {
148 return Err(anyhow::format_err!("duplicate flag: {name}"));
149 }
150 }
151
152 let program_spec = ProgramSpec::new(
153 program,
154 system_path,
155 option_bundling,
156 combined_format,
157 allowed_options,
158 args,
159 forbidden,
160 should_match
161 .map_or_else(Vec::new, |v| v.items.to_vec())
162 .into_iter()
163 .map(|v| v.items.to_vec())
164 .collect(),
165 should_not_match
166 .map_or_else(Vec::new, |v| v.items.to_vec())
167 .into_iter()
168 .map(|v| v.items.to_vec())
169 .collect(),
170 );
171
172 #[expect(clippy::unwrap_used)]
173 let policy_builder = eval
174 .extra
175 .as_ref()
176 .unwrap()
177 .downcast_ref::<PolicyBuilder>()
178 .unwrap();
179 policy_builder.add_program_spec(program_spec);
180 Ok(NoneType)
181 }
182
183 fn forbid_substrings(
184 strings: UnpackList<String>,
185 eval: &mut Evaluator,
186 ) -> anyhow::Result<NoneType> {
187 #[expect(clippy::unwrap_used)]
188 let policy_builder = eval
189 .extra
190 .as_ref()
191 .unwrap()
192 .downcast_ref::<PolicyBuilder>()
193 .unwrap();
194 policy_builder.add_forbidden_substrings(&strings.items.to_vec());
195 Ok(NoneType)
196 }
197
198 fn forbid_program_regex(
199 regex: String,
200 reason: String,
201 eval: &mut Evaluator,
202 ) -> anyhow::Result<NoneType> {
203 #[expect(clippy::unwrap_used)]
204 let policy_builder = eval
205 .extra
206 .as_ref()
207 .unwrap()
208 .downcast_ref::<PolicyBuilder>()
209 .unwrap();
210 let compiled_regex = regex_lite::Regex::new(®ex)?;
211 policy_builder.add_forbidden_program_regex(compiled_regex, reason);
212 Ok(NoneType)
213 }
214
215 fn opt(name: String, r#type: ArgMatcher, required: Option<bool>) -> anyhow::Result<Opt> {
216 Ok(Opt::new(
217 name,
218 OptMeta::Value(r#type.arg_type()),
219 required.unwrap_or(false),
220 ))
221 }
222
223 fn flag(name: String) -> anyhow::Result<Opt> {
224 Ok(Opt::new(name, OptMeta::Flag, false))
225 }
226}