1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::parser::{AstExpression, BinaryOp, Diagnostic, SourceSpan, UnaryOp};
4use serde::{Deserialize, Serialize};
5
6use crate::sema::profile::{BodyAccess, BodyTarget, Phase};
7
8mod oxirule;
9
10#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum TypeClass {
13 Null,
14 Bool,
15 Int,
16 Float,
17 String,
18 Bytes,
19 Array,
20 Object,
21 Dyn,
22 RegexLiteral,
23}
24
25#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum CapabilityKind {
28 Function,
29 Method,
30 UnaryOp,
31 BinaryOp,
32}
33
34#[derive(Debug, Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum RegexFlavor {
37 Default,
38 HeaderName,
39}
40
41#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
42pub struct RegexArgMeta {
43 pub index: usize,
44 pub flavor: RegexFlavor,
45}
46
47impl RegexArgMeta {
48 pub fn new(index: usize, flavor: RegexFlavor) -> Self {
49 Self { index, flavor }
50 }
51}
52
53#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CostModel {
56 Constant(u32),
57 LinearInput { factor: u32 },
58 LinearCollection { factor: u32 },
59 RegexMatch { factor: u32, precompiled: bool },
60}
61
62impl CostModel {
63 pub fn static_cost(&self) -> u64 {
64 match self {
65 Self::Constant(value) => u64::from(*value),
66 Self::LinearInput { factor }
67 | Self::LinearCollection { factor }
68 | Self::RegexMatch { factor, .. } => u64::from(*factor),
69 }
70 }
71}
72
73impl Default for CostModel {
74 fn default() -> Self {
75 Self::Constant(1)
76 }
77}
78
79#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
80pub struct VariableMeta {
81 pub name: String,
82 pub type_class: TypeClass,
83 pub phases: BTreeSet<Phase>,
84}
85
86impl VariableMeta {
87 pub fn new(name: impl Into<String>) -> Self {
88 Self {
89 name: name.into(),
90 type_class: TypeClass::Dyn,
91 phases: BTreeSet::new(),
92 }
93 }
94
95 pub fn with_type(mut self, type_class: TypeClass) -> Self {
96 self.type_class = type_class;
97 self
98 }
99
100 pub fn with_phases(mut self, phases: impl IntoIterator<Item = Phase>) -> Self {
101 self.phases = phases.into_iter().collect();
102 self
103 }
104
105 pub fn is_available_in(&self, phase: Phase) -> bool {
106 self.phases.is_empty() || self.phases.contains(&phase)
107 }
108}
109
110#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
111pub struct CapabilityMeta {
112 pub name: String,
113 pub kind: CapabilityKind,
114 pub arity: usize,
115 pub receiver: Option<TypeClass>,
116 pub args: Vec<TypeClass>,
117 pub result: TypeClass,
118 pub phases: BTreeSet<Phase>,
119 pub body_access: BodyAccess,
120 pub regex_args: Vec<RegexArgMeta>,
121 pub deterministic: bool,
122 pub side_effect_free: bool,
123 pub cost: CostModel,
124}
125
126impl CapabilityMeta {
127 pub fn function(name: impl Into<String>, arity: usize) -> Self {
128 Self::new(name, CapabilityKind::Function, arity)
129 }
130
131 pub fn method(name: impl Into<String>, arity: usize) -> Self {
132 Self::new(name, CapabilityKind::Method, arity)
133 }
134
135 pub fn unary_operator(op: UnaryOp) -> Self {
136 Self::new(op.as_str(), CapabilityKind::UnaryOp, 1)
137 }
138
139 pub fn binary_operator(op: BinaryOp) -> Self {
140 Self::new(op.as_str(), CapabilityKind::BinaryOp, 2)
141 }
142
143 pub fn with_phases(mut self, phases: impl IntoIterator<Item = Phase>) -> Self {
144 self.phases = phases.into_iter().collect();
145 self
146 }
147
148 pub fn with_receiver(mut self, receiver: TypeClass) -> Self {
149 self.receiver = Some(receiver);
150 self
151 }
152
153 pub fn with_args(mut self, args: impl IntoIterator<Item = TypeClass>) -> Self {
154 self.args = args.into_iter().collect();
155 self.arity = self.args.len();
156 self
157 }
158
159 pub fn with_result(mut self, result: TypeClass) -> Self {
160 self.result = result;
161 self
162 }
163
164 pub fn with_body_access(mut self, access: BodyAccess) -> Self {
165 self.body_access = access;
166 self
167 }
168
169 pub fn with_regex_arg(mut self, index: usize, flavor: RegexFlavor) -> Self {
170 self.regex_args.push(RegexArgMeta::new(index, flavor));
171 self
172 }
173
174 pub fn with_cost(mut self, cost: CostModel) -> Self {
175 self.cost = cost;
176 self
177 }
178
179 pub fn with_deterministic(mut self, deterministic: bool) -> Self {
180 self.deterministic = deterministic;
181 self
182 }
183
184 pub fn with_side_effect_free(mut self, side_effect_free: bool) -> Self {
185 self.side_effect_free = side_effect_free;
186 self
187 }
188
189 pub fn is_available_in(&self, phase: Phase) -> bool {
190 self.phases.is_empty() || self.phases.contains(&phase)
191 }
192
193 pub fn ticket(&self) -> CapabilityTicket {
194 CapabilityTicket::new(self.kind, self.name.clone(), self.arity)
195 }
196
197 fn new(name: impl Into<String>, kind: CapabilityKind, arity: usize) -> Self {
198 Self {
199 name: name.into(),
200 kind,
201 arity,
202 receiver: None,
203 args: vec![TypeClass::Dyn; arity],
204 result: TypeClass::Dyn,
205 phases: BTreeSet::new(),
206 body_access: BodyAccess::None,
207 regex_args: Vec::new(),
208 deterministic: true,
209 side_effect_free: true,
210 cost: CostModel::default(),
211 }
212 }
213}
214
215#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
216pub struct CapabilityTicket {
217 pub kind: CapabilityKind,
218 pub name: String,
219 pub arity: usize,
220}
221
222impl CapabilityTicket {
223 pub fn new(kind: CapabilityKind, name: impl Into<String>, arity: usize) -> Self {
224 Self {
225 kind,
226 name: name.into(),
227 arity,
228 }
229 }
230}
231
232impl Ord for CapabilityTicket {
233 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
234 (self.kind_order(), &self.name, self.arity).cmp(&(other.kind_order(), &other.name, other.arity))
235 }
236}
237
238impl PartialOrd for CapabilityTicket {
239 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
240 Some(self.cmp(other))
241 }
242}
243
244impl CapabilityTicket {
245 fn kind_order(&self) -> u8 {
246 match self.kind {
247 CapabilityKind::Function => 0,
248 CapabilityKind::Method => 1,
249 CapabilityKind::UnaryOp => 2,
250 CapabilityKind::BinaryOp => 3,
251 }
252 }
253}
254
255#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
256pub struct BodyPathRule {
257 pub path: Vec<String>,
258 pub target: BodyTarget,
259 pub access: BodyAccess,
260}
261
262impl BodyPathRule {
263 pub fn new(
264 path: impl IntoIterator<Item = impl Into<String>>,
265 target: BodyTarget,
266 access: BodyAccess,
267 ) -> Self {
268 Self {
269 path: path.into_iter().map(Into::into).collect(),
270 target,
271 access,
272 }
273 }
274}
275
276#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
277pub struct ExpressionFunction {
278 pub name: String,
279 pub params: Vec<String>,
280 pub expression: AstExpression,
281 #[serde(default)]
282 pub scope: ExpressionFunctionScope,
283}
284
285#[derive(
286 Debug, Clone, Copy, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
287)]
288#[serde(rename_all = "snake_case")]
289pub enum ExpressionFunctionScope {
290 #[default]
291 Global,
292 Local,
293}
294
295impl ExpressionFunctionScope {
296 pub fn call_scope(self) -> Self {
297 match self {
298 Self::Global => Self::Global,
299 Self::Local => Self::Local,
300 }
301 }
302}
303
304#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
305pub struct ExpressionFunctionDiagnostic {
306 pub message: String,
307 pub span: SourceSpan,
308}
309
310impl ExpressionFunctionDiagnostic {
311 fn new(message: impl Into<String>, span: SourceSpan) -> Self {
312 Self {
313 message: message.into(),
314 span,
315 }
316 }
317
318 pub fn diagnostic(&self) -> Diagnostic {
319 Diagnostic::new(self.message.clone(), self.span)
320 }
321}
322
323#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
324pub struct RuntimeSchema {
325 variables: BTreeMap<String, VariableMeta>,
326 functions: BTreeMap<String, BTreeMap<usize, CapabilityMeta>>,
327 methods: BTreeMap<String, BTreeMap<usize, CapabilityMeta>>,
328 unary_ops: BTreeMap<String, CapabilityMeta>,
329 binary_ops: BTreeMap<String, CapabilityMeta>,
330 body_paths: Vec<BodyPathRule>,
331 expression_functions: BTreeMap<String, ExpressionFunction>,
332 #[serde(default)]
333 local_expression_functions: BTreeMap<String, ExpressionFunction>,
334 #[serde(default)]
335 expression_function_diagnostics: Vec<ExpressionFunctionDiagnostic>,
336}
337
338impl RuntimeSchema {
339 pub fn new() -> Self {
340 Self::default()
341 }
342
343 pub fn waf() -> Self {
344 let mut schema = Self::new();
345 schema
346 .add_variable("Request")
347 .add_variable("Response")
348 .add_variable("Stream")
349 .add_waf_body_paths()
350 .add_waf_body_methods()
351 .add_waf_regex_methods();
352 schema
353 }
354
355 pub fn add_variable(&mut self, name: impl Into<String>) -> &mut Self {
356 self.add_variable_meta(VariableMeta::new(name))
357 }
358
359 pub fn add_variable_meta(&mut self, variable: VariableMeta) -> &mut Self {
360 self.variables.insert(variable.name.clone(), variable);
361 self
362 }
363
364 pub fn add_function(&mut self, name: impl Into<String>, arity: usize) -> &mut Self {
365 self.add_function_capability(CapabilityMeta::function(name, arity))
366 }
367
368 pub fn add_function_capability(&mut self, capability: CapabilityMeta) -> &mut Self {
369 self
370 .functions
371 .entry(capability.name.clone())
372 .or_default()
373 .insert(capability.arity, capability);
374 self
375 }
376
377 pub fn add_method(&mut self, name: impl Into<String>, arity: usize) -> &mut Self {
378 self.add_method_capability(CapabilityMeta::method(name, arity))
379 }
380
381 pub fn add_method_capability(&mut self, capability: CapabilityMeta) -> &mut Self {
382 self
383 .methods
384 .entry(capability.name.clone())
385 .or_default()
386 .insert(capability.arity, capability);
387 self
388 }
389
390 pub fn add_unary_operator_capability(&mut self, capability: CapabilityMeta) -> &mut Self {
391 self.unary_ops.insert(capability.name.clone(), capability);
392 self
393 }
394
395 pub fn add_binary_operator_capability(&mut self, capability: CapabilityMeta) -> &mut Self {
396 self.binary_ops.insert(capability.name.clone(), capability);
397 self
398 }
399
400 pub fn add_body_path(
401 &mut self,
402 path: impl IntoIterator<Item = impl Into<String>>,
403 target: BodyTarget,
404 access: BodyAccess,
405 ) -> &mut Self {
406 self
407 .body_paths
408 .push(BodyPathRule::new(path, target, access));
409 self
410 }
411
412 pub fn add_waf_body_paths(&mut self) -> &mut Self {
413 for root in ["Request", "Response"] {
414 let target = if root == "Request" {
415 BodyTarget::Request
416 } else {
417 BodyTarget::Response
418 };
419 self.add_body_path([root, "Body", "Size"], target, BodyAccess::SizeOnly);
420 self.add_body_path([root, "Body", "Bytes"], target, BodyAccess::PrefixBytes);
421 self.add_body_path([root, "Body", "Text"], target, BodyAccess::PrefixBytes);
422 self.add_body_path(
423 [root, "Body", "IsTruncated"],
424 target,
425 BodyAccess::PrefixBytes,
426 );
427 }
428 self.add_body_path(
429 ["Stream", "Payload"],
430 BodyTarget::Stream,
431 BodyAccess::PrefixBytes,
432 );
433 self
434 }
435
436 pub fn add_waf_body_methods(&mut self) -> &mut Self {
437 for (name, arity) in [
438 ("isFormat", 1),
439 ("isBinaryFormat", 1),
440 ("matchesFormat", 1),
441 ("contains", 1),
442 ("containsBytes", 1),
443 ("containsAny", 1),
444 ("matchesAny", 1),
445 ("scan", 1),
446 ("anomalyScore", 1),
447 ("malformedScore", 1),
448 ("promptInjectionScore", 0),
449 ] {
450 self.add_method_capability(
451 CapabilityMeta::method(name, arity).with_body_access(BodyAccess::PrefixBytes),
452 );
453 }
454 self.add_method_capability(
455 CapabilityMeta::method("matches", 1)
456 .with_body_access(BodyAccess::PrefixBytes)
457 .with_regex_arg(0, RegexFlavor::Default),
458 );
459 self
460 }
461
462 pub fn add_waf_regex_methods(&mut self) -> &mut Self {
463 self
464 .add_method_capability(
465 CapabilityMeta::method("anyValueMatches", 1).with_regex_arg(0, RegexFlavor::Default),
466 )
467 .add_method_capability(
468 CapabilityMeta::method("anyKeyMatches", 1).with_regex_arg(0, RegexFlavor::Default),
469 )
470 .add_method_capability(
471 CapabilityMeta::method("anyMatches", 1).with_regex_arg(0, RegexFlavor::Default),
472 )
473 .add_method_capability(
474 CapabilityMeta::method("anyNameMatches", 1)
475 .with_regex_arg(0, RegexFlavor::Default)
476 .with_regex_arg(0, RegexFlavor::HeaderName),
477 )
478 .add_method_capability(
479 CapabilityMeta::method("anyEntryMatches", 2)
480 .with_regex_arg(0, RegexFlavor::Default)
481 .with_regex_arg(0, RegexFlavor::HeaderName)
482 .with_regex_arg(1, RegexFlavor::Default),
483 )
484 .add_method_capability(
485 CapabilityMeta::method("allEntriesMatch", 2)
486 .with_regex_arg(0, RegexFlavor::HeaderName)
487 .with_regex_arg(1, RegexFlavor::Default),
488 );
489 self
490 }
491
492 pub fn add_expression_function(
493 &mut self,
494 name: impl Into<String>,
495 params: impl IntoIterator<Item = impl Into<String>>,
496 expression: AstExpression,
497 ) -> &mut Self {
498 self.add_scoped_expression_function(ExpressionFunctionScope::Global, name, params, expression)
499 }
500
501 pub fn add_local_expression_function(
502 &mut self,
503 name: impl Into<String>,
504 params: impl IntoIterator<Item = impl Into<String>>,
505 expression: AstExpression,
506 ) -> &mut Self {
507 self.add_scoped_expression_function(ExpressionFunctionScope::Local, name, params, expression)
508 }
509
510 pub fn add_scoped_expression_function(
511 &mut self,
512 scope: ExpressionFunctionScope,
513 name: impl Into<String>,
514 params: impl IntoIterator<Item = impl Into<String>>,
515 expression: AstExpression,
516 ) -> &mut Self {
517 let name = name.into();
518 let params = params.into_iter().map(Into::into).collect::<Vec<_>>();
519 self.add_function(name.clone(), params.len());
520 let target = match scope {
521 ExpressionFunctionScope::Global => &mut self.expression_functions,
522 ExpressionFunctionScope::Local => &mut self.local_expression_functions,
523 };
524 if target.contains_key(&name) {
525 self
526 .expression_function_diagnostics
527 .push(ExpressionFunctionDiagnostic::new(
528 format!("duplicate expression function {name} in {scope:?} scope"),
529 expression.span,
530 ));
531 }
532 target.insert(
533 name.clone(),
534 ExpressionFunction {
535 name,
536 params,
537 expression,
538 scope,
539 },
540 );
541 self
542 }
543
544 pub fn has_variable(&self, name: &str) -> bool {
545 self.variables.contains_key(name)
546 }
547
548 pub fn variables(&self) -> impl Iterator<Item = &VariableMeta> {
549 self.variables.values()
550 }
551
552 pub fn variable(&self, name: &str) -> Option<&VariableMeta> {
553 self.variables.get(name)
554 }
555
556 pub fn function_accepts(&self, name: &str, arity: usize) -> SignatureMatch {
557 signature_accepts(&self.functions, name, arity)
558 }
559
560 pub fn method_accepts(&self, name: &str, arity: usize) -> SignatureMatch {
561 signature_accepts(&self.methods, name, arity)
562 }
563
564 pub fn function_capability(&self, name: &str, arity: usize) -> Option<&CapabilityMeta> {
565 self
566 .functions
567 .get(name)
568 .and_then(|entries| entries.get(&arity))
569 }
570
571 pub fn method_capability(&self, name: &str, arity: usize) -> Option<&CapabilityMeta> {
572 self
573 .methods
574 .get(name)
575 .and_then(|entries| entries.get(&arity))
576 }
577
578 pub fn unary_operator_capability(&self, op: UnaryOp) -> Option<&CapabilityMeta> {
579 self.unary_ops.get(op.as_str())
580 }
581
582 pub fn binary_operator_capability(&self, op: BinaryOp) -> Option<&CapabilityMeta> {
583 self.binary_ops.get(op.as_str())
584 }
585
586 pub fn function_capabilities(&self) -> impl Iterator<Item = &CapabilityMeta> {
587 self.functions.values().flat_map(BTreeMap::values)
588 }
589
590 pub fn method_capabilities(&self) -> impl Iterator<Item = &CapabilityMeta> {
591 self.methods.values().flat_map(BTreeMap::values)
592 }
593
594 pub fn unary_operator_capabilities(&self) -> impl Iterator<Item = &CapabilityMeta> {
595 self.unary_ops.values()
596 }
597
598 pub fn binary_operator_capabilities(&self) -> impl Iterator<Item = &CapabilityMeta> {
599 self.binary_ops.values()
600 }
601
602 pub fn body_access_for_path(&self, path: &[String]) -> Option<(BodyTarget, BodyAccess)> {
603 self
604 .body_paths
605 .iter()
606 .find(|rule| rule.path == path)
607 .map(|rule| (rule.target, rule.access))
608 }
609
610 pub fn expression_function(&self, name: &str) -> Option<&ExpressionFunction> {
611 self.expression_function_for_scope(name, ExpressionFunctionScope::Local)
612 }
613
614 pub fn expression_function_for_scope(
615 &self,
616 name: &str,
617 scope: ExpressionFunctionScope,
618 ) -> Option<&ExpressionFunction> {
619 match scope {
620 ExpressionFunctionScope::Global => self.expression_functions.get(name),
621 ExpressionFunctionScope::Local => self
622 .local_expression_functions
623 .get(name)
624 .or_else(|| self.expression_functions.get(name)),
625 }
626 }
627
628 pub fn expression_functions(&self) -> impl Iterator<Item = &ExpressionFunction> {
629 self
630 .expression_functions
631 .values()
632 .chain(self.local_expression_functions.values())
633 }
634
635 pub fn expression_function_diagnostics(&self) -> &[ExpressionFunctionDiagnostic] {
636 &self.expression_function_diagnostics
637 }
638}
639
640#[derive(Debug, Clone, Copy, Eq, PartialEq)]
641pub enum SignatureMatch {
642 Unknown,
643 ArityMismatch,
644 Matches,
645}
646
647fn signature_accepts(
648 signatures: &BTreeMap<String, BTreeMap<usize, CapabilityMeta>>,
649 name: &str,
650 arity: usize,
651) -> SignatureMatch {
652 match signatures.get(name) {
653 Some(accepted) if accepted.contains_key(&arity) => SignatureMatch::Matches,
654 Some(_) => SignatureMatch::ArityMismatch,
655 None => SignatureMatch::Unknown,
656 }
657}