Skip to main content

online_dsl_forge/sema/
profile.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
6#[serde(rename_all = "snake_case")]
7pub enum SecurityProfileId {
8  GenericSafe,
9  GenericTransform,
10  WafRequest,
11  WafResponse,
12  WafStream,
13  MitigationField,
14  Custom(String),
15}
16
17#[derive(Debug, Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Phase {
20  Generic,
21  Request,
22  Response,
23  Stream,
24}
25
26#[derive(Debug, Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
27#[serde(rename_all = "snake_case")]
28pub enum BodyTarget {
29  Request,
30  Response,
31  Stream,
32}
33
34#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum BodyAccess {
37  #[default]
38  None,
39  SizeOnly,
40  PrefixBytes,
41}
42
43impl BodyAccess {
44  pub fn merge(self, other: Self) -> Self {
45    self.max(other)
46  }
47
48  pub fn allows(self, needed: Self) -> bool {
49    self >= needed
50  }
51
52  pub fn reads_payload(self) -> bool {
53    matches!(self, Self::PrefixBytes)
54  }
55}
56
57#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)]
58#[serde(rename_all = "snake_case")]
59pub enum RegexPolicy {
60  Forbid,
61  LiteralOnlyPrecompiled,
62  DynamicWithBudget,
63}
64
65#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)]
66#[serde(rename_all = "snake_case")]
67pub enum Determinism {
68  Required,
69  BestEffort,
70}
71
72#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
73pub struct BodyNeedSummary {
74  pub request: BodyAccess,
75  pub response: BodyAccess,
76  pub stream: BodyAccess,
77}
78
79impl BodyNeedSummary {
80  pub fn none() -> Self {
81    Self::default()
82  }
83
84  pub fn all(access: BodyAccess) -> Self {
85    Self {
86      request: access,
87      response: access,
88      stream: access,
89    }
90  }
91
92  pub fn merge(self, other: Self) -> Self {
93    Self {
94      request: self.request.merge(other.request),
95      response: self.response.merge(other.response),
96      stream: self.stream.merge(other.stream),
97    }
98  }
99
100  pub fn merge_target(&mut self, target: BodyTarget, access: BodyAccess) {
101    match target {
102      BodyTarget::Request => self.request = self.request.merge(access),
103      BodyTarget::Response => self.response = self.response.merge(access),
104      BodyTarget::Stream => self.stream = self.stream.merge(access),
105    }
106  }
107
108  pub fn reads_payload(self) -> bool {
109    self.request.reads_payload() || self.response.reads_payload() || self.stream.reads_payload()
110  }
111
112  pub fn allows(self, needed: Self) -> bool {
113    self.request.allows(needed.request)
114      && self.response.allows(needed.response)
115      && self.stream.allows(needed.stream)
116  }
117}
118
119#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
120pub struct SecurityProfile {
121  pub id: SecurityProfileId,
122  pub allowed_phases: BTreeSet<Phase>,
123  pub max_ast_nodes: usize,
124  pub max_call_depth: usize,
125  pub default_regex_policy: RegexPolicy,
126  pub max_cost_units: u64,
127  pub determinism: Determinism,
128  pub fail_closed: bool,
129  #[serde(default)]
130  pub body_access_limit: Option<BodyNeedSummary>,
131}
132
133impl SecurityProfile {
134  pub fn generic_safe() -> Self {
135    Self {
136      id: SecurityProfileId::GenericSafe,
137      allowed_phases: BTreeSet::from([Phase::Generic]),
138      max_ast_nodes: 4096,
139      max_call_depth: 64,
140      default_regex_policy: RegexPolicy::DynamicWithBudget,
141      max_cost_units: 100_000,
142      determinism: Determinism::Required,
143      fail_closed: true,
144      body_access_limit: None,
145    }
146  }
147
148  pub fn generic_transform() -> Self {
149    Self {
150      id: SecurityProfileId::GenericTransform,
151      max_ast_nodes: 8192,
152      max_call_depth: 128,
153      max_cost_units: 250_000,
154      ..Self::generic_safe()
155    }
156  }
157
158  pub fn waf_request() -> Self {
159    Self::waf(SecurityProfileId::WafRequest, Phase::Request)
160  }
161
162  pub fn waf_response() -> Self {
163    Self::waf(SecurityProfileId::WafResponse, Phase::Response)
164  }
165
166  pub fn waf_stream() -> Self {
167    Self::waf(SecurityProfileId::WafStream, Phase::Stream)
168  }
169
170  pub fn oxirule_waf_request() -> Self {
171    Self::oxirule_waf(SecurityProfileId::WafRequest, Phase::Request)
172  }
173
174  pub fn oxirule_waf_response() -> Self {
175    Self::oxirule_waf(SecurityProfileId::WafResponse, Phase::Response)
176  }
177
178  pub fn oxirule_waf_stream() -> Self {
179    Self::oxirule_waf(SecurityProfileId::WafStream, Phase::Stream)
180  }
181
182  pub fn mitigation_field(phase: Phase) -> Self {
183    let allowed_phases = BTreeSet::from([phase]);
184    Self {
185      id: SecurityProfileId::MitigationField,
186      allowed_phases,
187      max_ast_nodes: 2048,
188      max_call_depth: 32,
189      default_regex_policy: RegexPolicy::LiteralOnlyPrecompiled,
190      max_cost_units: 50_000,
191      determinism: Determinism::Required,
192      fail_closed: true,
193      body_access_limit: None,
194    }
195  }
196
197  pub fn with_regex_policy(mut self, policy: RegexPolicy) -> Self {
198    self.default_regex_policy = policy;
199    self
200  }
201
202  pub fn with_body_access_limit(mut self, limit: Option<BodyNeedSummary>) -> Self {
203    self.body_access_limit = limit;
204    self
205  }
206
207  pub fn deny_body_access(self) -> Self {
208    self.with_body_access_limit(Some(BodyNeedSummary::none()))
209  }
210
211  pub fn allow_body_access(self) -> Self {
212    self.with_body_access_limit(None)
213  }
214
215  pub fn active_phase(&self) -> Option<Phase> {
216    if self.allowed_phases.len() == 1 {
217      self.allowed_phases.iter().next().copied()
218    } else {
219      None
220    }
221  }
222
223  fn waf(id: SecurityProfileId, phase: Phase) -> Self {
224    Self {
225      id,
226      allowed_phases: BTreeSet::from([phase]),
227      max_ast_nodes: 4096,
228      max_call_depth: 64,
229      default_regex_policy: RegexPolicy::LiteralOnlyPrecompiled,
230      max_cost_units: 100_000,
231      determinism: Determinism::Required,
232      fail_closed: true,
233      body_access_limit: None,
234    }
235  }
236
237  fn oxirule_waf(id: SecurityProfileId, phase: Phase) -> Self {
238    Self {
239      default_regex_policy: RegexPolicy::DynamicWithBudget,
240      ..Self::waf(id, phase)
241    }
242  }
243}
244
245impl Default for SecurityProfile {
246  fn default() -> Self {
247    Self::generic_safe()
248  }
249}