1mod body_need;
2mod functions;
3mod phase;
4mod support;
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use crate::parser::{
9 AstExpression, BinaryOp, Diagnostic, DiagnosticReport, ExprKind, SourceSpan, UnaryOp,
10};
11use serde::{Deserialize, Serialize};
12
13use crate::sema::dialect::ExpressionDialect;
14use crate::sema::profile::{
15 BodyNeedSummary, Determinism, RegexPolicy, SecurityProfile, SecurityProfileId,
16};
17use crate::sema::schema::{
18 CapabilityMeta, CapabilityTicket, ExpressionFunctionScope, RuntimeSchema, SignatureMatch,
19};
20use crate::sema::verified::{
21 CompiledExpression, CompiledRegexCache, RegexLiteral, VerifiedExprKind, VerifiedExpression,
22 VerifiedProgram, VerifiedProgramParts,
23};
24use support::{
25 ArgsAnalysis, ExprAnalysis, LocalBinding, ObjectOrigin, member_origin, string_literal,
26};
27
28#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
29pub struct CompileOptions {
30 pub allow_unknown_variables: bool,
31 pub allow_unknown_functions: bool,
32 pub allow_unknown_methods: bool,
33}
34
35#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
36pub enum ExpressionFunctionMode {
37 #[default]
38 Inline,
39 CallFrame,
40}
41
42#[derive(Debug, Clone)]
43pub struct Analyzer {
44 profile: SecurityProfile,
45 options: CompileOptions,
46 dialect: ExpressionDialect,
47 expression_function_scope: ExpressionFunctionScope,
48 expression_function_mode: ExpressionFunctionMode,
49}
50
51impl Analyzer {
52 pub fn new(profile: SecurityProfile) -> Self {
53 Self {
54 profile,
55 options: CompileOptions::default(),
56 dialect: ExpressionDialect::default(),
57 expression_function_scope: ExpressionFunctionScope::Local,
58 expression_function_mode: ExpressionFunctionMode::default(),
59 }
60 }
61
62 pub fn with_options(mut self, options: CompileOptions) -> Self {
63 self.options = options;
64 self
65 }
66
67 pub fn with_dialect(mut self, dialect: ExpressionDialect) -> Self {
68 self.dialect = dialect;
69 self
70 }
71
72 pub fn with_expression_function_scope(mut self, scope: ExpressionFunctionScope) -> Self {
73 self.expression_function_scope = scope;
74 self
75 }
76
77 pub fn with_expression_function_mode(mut self, mode: ExpressionFunctionMode) -> Self {
78 self.expression_function_mode = mode;
79 self
80 }
81
82 pub fn analyze(
83 &self,
84 expression: &AstExpression,
85 schema: &RuntimeSchema,
86 ) -> Result<VerifiedProgram, DiagnosticReport> {
87 let mut state = AnalyzeState::new(self, schema);
88 self.dialect.validate(expression, &mut state.diagnostics);
89 state.validate_function_graph();
90 let mut analysis = state.analyze_expression(expression, 0);
91 state.merge_body_access_for_exposed_origin(&mut analysis.body_need, analysis.origin);
92 state.validate_program_bounds(&analysis, expression.span);
93
94 if state.diagnostics.is_empty() {
95 Ok(VerifiedProgram::new(VerifiedProgramParts {
96 ast: expression.clone(),
97 root: analysis.expr,
98 profile: self.profile.clone(),
99 body_need: analysis.body_need,
100 static_cost_upper_bound: analysis.cost,
101 regex_literals: state.regex_literals,
102 regex_cache: state.regex_cache,
103 required_capabilities: state.required_capabilities,
104 required_capability_metadata: state.required_capability_metadata,
105 }))
106 } else {
107 Err(DiagnosticReport::new(state.diagnostics))
108 }
109 }
110}
111
112pub fn compile_expression(
113 expression: &AstExpression,
114 schema: &RuntimeSchema,
115 options: CompileOptions,
116) -> Result<CompiledExpression, DiagnosticReport> {
117 Analyzer::new(SecurityProfile::generic_safe())
118 .with_options(options)
119 .analyze(expression, schema)
120 .map(CompiledExpression::new)
121}
122
123struct AnalyzeState<'a> {
124 analyzer: &'a Analyzer,
125 schema: &'a RuntimeSchema,
126 diagnostics: Vec<Diagnostic>,
127 regex_literals: Vec<RegexLiteral>,
128 regex_cache: CompiledRegexCache,
129 required_capabilities: BTreeSet<CapabilityTicket>,
130 required_capability_metadata: BTreeMap<CapabilityTicket, CapabilityMeta>,
131 active_functions: Vec<(ExpressionFunctionScope, String)>,
132 local_bindings: Vec<BTreeMap<String, LocalBinding>>,
133}
134
135impl<'a> AnalyzeState<'a> {
136 fn new(analyzer: &'a Analyzer, schema: &'a RuntimeSchema) -> Self {
137 Self {
138 analyzer,
139 schema,
140 diagnostics: Vec::new(),
141 regex_literals: Vec::new(),
142 regex_cache: CompiledRegexCache::default(),
143 required_capabilities: BTreeSet::new(),
144 required_capability_metadata: BTreeMap::new(),
145 active_functions: Vec::new(),
146 local_bindings: Vec::new(),
147 }
148 }
149
150 fn validate_program_bounds(&mut self, analysis: &ExprAnalysis, span: SourceSpan) {
151 if analysis.nodes > self.analyzer.profile.max_ast_nodes {
152 self
153 .diagnostics
154 .push(Diagnostic::new("AST node limit exceeded", span));
155 }
156 if analysis.cost > self.analyzer.profile.max_cost_units {
157 self
158 .diagnostics
159 .push(Diagnostic::new("static cost limit exceeded", span));
160 }
161 if let Some(limit) = self.analyzer.profile.body_access_limit
162 && !limit.allows(analysis.body_need)
163 {
164 self.diagnostics.push(Diagnostic::new(
165 "body access limit exceeded by profile",
166 span,
167 ));
168 }
169 if matches!(self.analyzer.profile.id, SecurityProfileId::MitigationField)
170 && (analysis.mitigation_payload || analysis.body_need.reads_payload())
171 {
172 self.diagnostics.push(Diagnostic::new(
173 "MitigationField cannot read request, response, or stream body bytes",
174 span,
175 ));
176 }
177 }
178
179 fn analyze_expression(&mut self, expression: &AstExpression, depth: usize) -> ExprAnalysis {
180 if depth > self.analyzer.profile.max_call_depth {
181 self.diagnostics.push(Diagnostic::new(
182 "semantic call depth limit exceeded",
183 expression.span,
184 ));
185 }
186
187 match &expression.kind {
188 ExprKind::Null => ExprAnalysis::leaf(
189 VerifiedExpression::new(VerifiedExprKind::Null, expression.span),
190 None,
191 ),
192 ExprKind::Bool { value } => ExprAnalysis::leaf(
193 VerifiedExpression::new(VerifiedExprKind::Bool(*value), expression.span),
194 None,
195 ),
196 ExprKind::Int { value } => ExprAnalysis::leaf(
197 VerifiedExpression::new(VerifiedExprKind::Int(*value), expression.span),
198 None,
199 ),
200 ExprKind::Float { value } => ExprAnalysis::leaf(
201 VerifiedExpression::new(VerifiedExprKind::Float(*value), expression.span),
202 None,
203 ),
204 ExprKind::String { value } => ExprAnalysis::leaf(
205 VerifiedExpression::new(VerifiedExprKind::String(value.clone()), expression.span),
206 None,
207 ),
208 ExprKind::Identifier { name } => self.analyze_identifier(name, expression.span),
209 ExprKind::Array { items } => self.analyze_array(items, expression.span, depth),
210 ExprKind::Member { receiver, name } => {
211 self.analyze_member(receiver, name, expression.span, depth)
212 }
213 ExprKind::FunctionCall { name, args } => {
214 self.analyze_function_call(name, args, expression.span, depth)
215 }
216 ExprKind::MethodCall {
217 receiver,
218 name,
219 args,
220 } => self.analyze_method_call(receiver, name, args, expression.span, depth),
221 ExprKind::Unary { op, expr } => self.analyze_unary(*op, expr, expression.span, depth),
222 ExprKind::Binary { left, op, right } => {
223 self.analyze_binary(left, *op, right, expression.span, depth)
224 }
225 }
226 }
227
228 fn analyze_identifier(&mut self, name: &str, span: SourceSpan) -> ExprAnalysis {
229 if let Some(binding) = self.local_binding(name).cloned() {
230 let mut analysis = ExprAnalysis::leaf(
231 VerifiedExpression::new(VerifiedExprKind::Identifier(name.to_string()), span),
232 binding.origin,
233 )
234 .with_path_option(binding.path)
235 .with_mitigation_payload(binding.mitigation_payload);
236 self.merge_body_access_for_path(&mut analysis.body_need, analysis.path.as_deref());
237 return analysis;
238 }
239 if !self.analyzer.options.allow_unknown_variables && !self.schema.has_variable(name) {
240 self
241 .diagnostics
242 .push(Diagnostic::new(format!("unknown variable {name}"), span));
243 }
244 self.validate_variable_phase(name, span);
245 let mut analysis = ExprAnalysis::leaf(
246 VerifiedExpression::new(VerifiedExprKind::Identifier(name.to_string()), span),
247 ObjectOrigin::root(name),
248 )
249 .with_path(vec![name.to_string()]);
250 self.merge_body_access_for_path(&mut analysis.body_need, analysis.path.as_deref());
251 analysis
252 }
253
254 fn analyze_array(
255 &mut self,
256 items: &[AstExpression],
257 span: SourceSpan,
258 depth: usize,
259 ) -> ExprAnalysis {
260 let mut body_need = BodyNeedSummary::default();
261 let mut mitigation_payload = false;
262 let mut nodes = 1;
263 let mut cost = 1;
264 let items = items
265 .iter()
266 .map(|item| {
267 let analysis = self.analyze_expression(item, depth + 1);
268 body_need = body_need
269 .merge(self.body_need_for_consumed_analysis(analysis.body_need, analysis.origin));
270 mitigation_payload |= analysis.mitigation_payload;
271 nodes += analysis.nodes;
272 cost += analysis.cost;
273 analysis.expr
274 })
275 .collect();
276 ExprAnalysis::new(
277 VerifiedExpression::new(VerifiedExprKind::Array(items), span),
278 None,
279 None,
280 body_need,
281 nodes,
282 cost,
283 )
284 .with_mitigation_payload(mitigation_payload)
285 }
286
287 fn analyze_member(
288 &mut self,
289 receiver: &AstExpression,
290 name: &str,
291 span: SourceSpan,
292 depth: usize,
293 ) -> ExprAnalysis {
294 let receiver = self.analyze_expression(receiver, depth + 1);
295 let path = receiver.path.as_ref().map(|path| {
296 let mut path = path.clone();
297 path.push(name.to_string());
298 path
299 });
300 let origin = receiver
301 .origin
302 .and_then(|origin| member_origin(origin, name));
303 let mut body_need = receiver.body_need;
304 self.merge_body_access_for_origin(&mut body_need, receiver.origin, name, span);
305 if let Some(path) = &path
306 && let Some((target, access)) = self.schema.body_access_for_path(path)
307 {
308 body_need.merge_target(target, access);
309 }
310 self.validate_origin_phase(origin, span);
311 let mitigation_payload = receiver.mitigation_payload
312 || origin.is_some_and(ObjectOrigin::is_mitigation_payload_boundary);
313
314 ExprAnalysis::new(
315 VerifiedExpression::new(
316 VerifiedExprKind::Member {
317 receiver: Box::new(receiver.expr),
318 name: name.to_string(),
319 },
320 span,
321 ),
322 origin,
323 path,
324 body_need,
325 receiver.nodes + 1,
326 receiver.cost + 1,
327 )
328 .with_mitigation_payload(mitigation_payload)
329 }
330
331 fn analyze_function_call(
332 &mut self,
333 name: &str,
334 args: &[AstExpression],
335 span: SourceSpan,
336 depth: usize,
337 ) -> ExprAnalysis {
338 if let Some(function) = self
339 .schema
340 .expression_function_for_scope(name, self.current_function_scope())
341 {
342 return self.analyze_expression_function(function, args, span, depth);
343 }
344
345 let capability = self.validate_call(
346 "function",
347 name,
348 args.len(),
349 self.schema.function_accepts(name, args.len()),
350 self.analyzer.options.allow_unknown_functions,
351 span,
352 );
353 let args_analysis = self.analyze_args(args, depth);
354 if let Some(capability) = capability {
355 self.validate_capability(capability, span);
356 self.validate_regex_args(capability, args, span);
357 self.require_capability(capability);
358 }
359 let capability_ticket = capability.map(CapabilityMeta::ticket);
360 let body_need = args_analysis.consumed_body_need;
361 ExprAnalysis::new(
362 verified_with_capability(
363 VerifiedExpression::new(
364 VerifiedExprKind::FunctionCall {
365 name: name.to_string(),
366 args: args_analysis.exprs,
367 },
368 span,
369 ),
370 capability_ticket,
371 ),
372 None,
373 None,
374 body_need,
375 args_analysis.nodes + 1,
376 args_analysis.cost + capability.map_or(1, |capability| capability.cost.static_cost()),
377 )
378 .with_mitigation_payload(args_analysis.mitigation_payload)
379 }
380
381 fn analyze_method_call(
382 &mut self,
383 receiver: &AstExpression,
384 name: &str,
385 args: &[AstExpression],
386 span: SourceSpan,
387 depth: usize,
388 ) -> ExprAnalysis {
389 let receiver = self.analyze_expression(receiver, depth + 1);
390 let capability = self.validate_call(
391 "method",
392 name,
393 args.len(),
394 self.schema.method_accepts(name, args.len()),
395 self.analyzer.options.allow_unknown_methods,
396 span,
397 );
398 let args_analysis = self.analyze_args(args, depth);
399 let receiver_body_need =
400 self.body_need_for_consumed_analysis(receiver.body_need, receiver.origin);
401 let mut body_need = receiver_body_need.merge(args_analysis.consumed_body_need);
402 let mitigation_payload = receiver.mitigation_payload || args_analysis.mitigation_payload;
403 if let Some(capability) = capability {
404 self.validate_capability(capability, span);
405 self.validate_regex_args(capability, args, span);
406 self.merge_body_access_for_method(&mut body_need, receiver.origin, capability);
407 self.require_capability(capability);
408 }
409 let capability_ticket = capability.map(CapabilityMeta::ticket);
410
411 ExprAnalysis::new(
412 verified_with_capability(
413 VerifiedExpression::new(
414 VerifiedExprKind::MethodCall {
415 receiver: Box::new(receiver.expr),
416 name: name.to_string(),
417 args: args_analysis.exprs,
418 },
419 span,
420 ),
421 capability_ticket,
422 ),
423 None,
424 None,
425 body_need,
426 receiver.nodes + args_analysis.nodes + 1,
427 receiver.cost
428 + args_analysis.cost
429 + capability.map_or(1, |capability| capability.cost.static_cost()),
430 )
431 .with_mitigation_payload(mitigation_payload)
432 }
433
434 fn analyze_unary(
435 &mut self,
436 op: UnaryOp,
437 expr: &AstExpression,
438 span: SourceSpan,
439 depth: usize,
440 ) -> ExprAnalysis {
441 let expr = self.analyze_expression(expr, depth + 1);
442 let capability = self
443 .schema
444 .unary_operator_capability(op)
445 .cloned()
446 .unwrap_or_else(|| CapabilityMeta::unary_operator(op));
447 self.validate_capability(&capability, span);
448 self.require_capability(&capability);
449 let ticket = capability.ticket();
450 let body_need = self.body_need_for_consumed_analysis(expr.body_need, expr.origin);
451 ExprAnalysis::new(
452 VerifiedExpression::new(
453 VerifiedExprKind::Unary {
454 op,
455 expr: Box::new(expr.expr),
456 },
457 span,
458 )
459 .with_capability_ticket(ticket),
460 None,
461 None,
462 body_need,
463 expr.nodes + 1,
464 expr.cost + capability.cost.static_cost(),
465 )
466 .with_mitigation_payload(expr.mitigation_payload)
467 }
468
469 fn analyze_binary(
470 &mut self,
471 left: &AstExpression,
472 op: BinaryOp,
473 right: &AstExpression,
474 span: SourceSpan,
475 depth: usize,
476 ) -> ExprAnalysis {
477 let left = self.analyze_expression(left, depth + 1);
478 let right = self.analyze_expression(right, depth + 1);
479 let capability = self
480 .schema
481 .binary_operator_capability(op)
482 .cloned()
483 .unwrap_or_else(|| CapabilityMeta::binary_operator(op));
484 self.validate_capability(&capability, span);
485 self.require_capability(&capability);
486 let ticket = capability.ticket();
487 let left_body_need = self.body_need_for_consumed_analysis(left.body_need, left.origin);
488 let right_body_need = self.body_need_for_consumed_analysis(right.body_need, right.origin);
489 ExprAnalysis::new(
490 VerifiedExpression::new(
491 VerifiedExprKind::Binary {
492 left: Box::new(left.expr),
493 op,
494 right: Box::new(right.expr),
495 },
496 span,
497 )
498 .with_capability_ticket(ticket),
499 None,
500 None,
501 left_body_need.merge(right_body_need),
502 left.nodes + right.nodes + 1,
503 left.cost + right.cost + capability.cost.static_cost(),
504 )
505 .with_mitigation_payload(left.mitigation_payload || right.mitigation_payload)
506 }
507
508 fn analyze_args(&mut self, args: &[AstExpression], depth: usize) -> ArgsAnalysis {
509 let mut body_need = BodyNeedSummary::default();
510 let mut consumed_body_need = BodyNeedSummary::default();
511 let mut mitigation_payload = false;
512 let mut nodes = 0;
513 let mut cost = 0;
514 let exprs = args
515 .iter()
516 .map(|arg| {
517 let analysis = self.analyze_expression(arg, depth + 1);
518 let binding = LocalBinding::from_analysis(&analysis);
519 body_need = body_need.merge(analysis.body_need);
520 consumed_body_need = consumed_body_need
521 .merge(self.body_need_for_consumed_analysis(analysis.body_need, analysis.origin));
522 mitigation_payload |= analysis.mitigation_payload;
523 nodes += analysis.nodes;
524 cost += analysis.cost;
525 (analysis.expr, binding)
526 })
527 .collect::<Vec<_>>();
528 let (exprs, bindings) = exprs.into_iter().unzip();
529 ArgsAnalysis {
530 exprs,
531 bindings,
532 body_need,
533 consumed_body_need,
534 mitigation_payload,
535 nodes,
536 cost,
537 }
538 }
539
540 fn local_binding(&self, name: &str) -> Option<&LocalBinding> {
541 self
542 .local_bindings
543 .iter()
544 .rev()
545 .find_map(|bindings| bindings.get(name))
546 }
547
548 fn validate_call(
549 &mut self,
550 kind: &'static str,
551 name: &str,
552 arity: usize,
553 result: SignatureMatch,
554 allow_unknown: bool,
555 span: SourceSpan,
556 ) -> Option<&'a CapabilityMeta> {
557 match result {
558 SignatureMatch::Matches => {
559 if kind == "function" {
560 self.schema.function_capability(name, arity)
561 } else {
562 self.schema.method_capability(name, arity)
563 }
564 }
565 SignatureMatch::Unknown if allow_unknown => None,
566 SignatureMatch::Unknown => {
567 self
568 .diagnostics
569 .push(Diagnostic::new(format!("unknown {kind} {name}"), span));
570 None
571 }
572 SignatureMatch::ArityMismatch => {
573 self.diagnostics.push(Diagnostic::new(
574 format!("{kind} {name} does not accept {arity} arguments"),
575 span,
576 ));
577 None
578 }
579 }
580 }
581
582 fn validate_regex_args(
583 &mut self,
584 capability: &CapabilityMeta,
585 args: &[AstExpression],
586 span: SourceSpan,
587 ) {
588 for regex_arg in &capability.regex_args {
589 let Some(arg) = args.get(regex_arg.index) else {
590 continue;
591 };
592 match self.analyzer.profile.default_regex_policy {
593 RegexPolicy::Forbid => self.diagnostics.push(Diagnostic::new(
594 "regex arguments are forbidden by profile",
595 span,
596 )),
597 RegexPolicy::LiteralOnlyPrecompiled => {
598 let Some(pattern) = string_literal(arg) else {
599 self.diagnostics.push(Diagnostic::new(
600 "regex argument must be a string literal",
601 arg.span,
602 ));
603 continue;
604 };
605 let literal = RegexLiteral {
606 pattern,
607 flavor: regex_arg.flavor,
608 span: arg.span,
609 };
610 if let Err(error) = self.regex_cache.insert(&literal) {
611 self.diagnostics.push(Diagnostic::new(
612 format!("invalid regex pattern: {error}"),
613 arg.span,
614 ));
615 } else {
616 self.regex_literals.push(literal);
617 }
618 }
619 RegexPolicy::DynamicWithBudget => {
620 if let Some(pattern) = string_literal(arg) {
621 let literal = RegexLiteral {
622 pattern,
623 flavor: regex_arg.flavor,
624 span: arg.span,
625 };
626 if self.regex_cache.insert(&literal).is_ok() {
627 self.regex_literals.push(literal);
628 }
629 }
630 }
631 }
632 }
633 }
634
635 fn validate_capability(&mut self, capability: &CapabilityMeta, span: SourceSpan) {
636 self.validate_capability_phase(capability, span);
637 if matches!(self.analyzer.profile.determinism, Determinism::Required) {
638 if !capability.deterministic {
639 self.diagnostics.push(Diagnostic::new(
640 format!(
641 "{} {} is non-deterministic but profile requires determinism",
642 support::capability_kind_label(capability.kind),
643 capability.name
644 ),
645 span,
646 ));
647 }
648 if !capability.side_effect_free {
649 self.diagnostics.push(Diagnostic::new(
650 format!(
651 "{} {} has side effects but profile requires side-effect-free capabilities",
652 support::capability_kind_label(capability.kind),
653 capability.name
654 ),
655 span,
656 ));
657 }
658 }
659 }
660
661 fn require_capability(&mut self, capability: &CapabilityMeta) {
662 let ticket = capability.ticket();
663 self.required_capabilities.insert(ticket.clone());
664 self
665 .required_capability_metadata
666 .insert(ticket, capability.clone());
667 }
668}
669
670fn verified_with_capability(
671 expression: VerifiedExpression,
672 ticket: Option<CapabilityTicket>,
673) -> VerifiedExpression {
674 if let Some(ticket) = ticket {
675 expression.with_capability_ticket(ticket)
676 } else {
677 expression
678 }
679}