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}