sentinel_modsec/engine/
intervention.rs1use super::phase::Phase;
4use crate::actions::RuleMetadata;
5
6#[derive(Debug, Clone)]
8pub struct Intervention {
9 pub status: u16,
11 pub url: Option<String>,
13 pub log: Option<String>,
15 pub rule_ids: Vec<String>,
17 pub phase: Phase,
19 pub drop_connection: bool,
21 pub metadata: Vec<RuleMetadata>,
23}
24
25impl Intervention {
26 pub fn new(status: u16, phase: Phase) -> Self {
28 Self {
29 status,
30 url: None,
31 log: None,
32 rule_ids: Vec::new(),
33 phase,
34 drop_connection: false,
35 metadata: Vec::new(),
36 }
37 }
38
39 pub fn deny(status: u16, phase: Phase, rule_id: Option<String>) -> Self {
41 let mut intervention = Self::new(status, phase);
42 if let Some(id) = rule_id {
43 intervention.rule_ids.push(id);
44 }
45 intervention
46 }
47
48 pub fn redirect(url: String, phase: Phase, rule_id: Option<String>) -> Self {
50 let mut intervention = Self::new(302, phase);
51 intervention.url = Some(url);
52 if let Some(id) = rule_id {
53 intervention.rule_ids.push(id);
54 }
55 intervention
56 }
57
58 pub fn drop(phase: Phase, rule_id: Option<String>) -> Self {
60 let mut intervention = Self::new(444, phase);
61 intervention.drop_connection = true;
62 if let Some(id) = rule_id {
63 intervention.rule_ids.push(id);
64 }
65 intervention
66 }
67
68 pub fn add_rule_id(&mut self, id: String) {
70 self.rule_ids.push(id);
71 }
72
73 pub fn add_metadata(&mut self, metadata: RuleMetadata) {
75 if let Some(ref id) = metadata.id {
76 self.rule_ids.push(id.clone());
77 }
78 if let Some(ref msg) = metadata.msg {
79 if self.log.is_none() {
80 self.log = Some(msg.clone());
81 }
82 }
83 self.metadata.push(metadata);
84 }
85
86 pub fn set_log(&mut self, log: String) {
88 self.log = Some(log);
89 }
90
91 pub fn format_log(&self) -> String {
93 let mut parts = vec![format!("[status {}]", self.status)];
94
95 if !self.rule_ids.is_empty() {
96 parts.push(format!("[rule_ids: {}]", self.rule_ids.join(", ")));
97 }
98
99 if let Some(ref log) = self.log {
100 parts.push(format!("[msg: {}]", log));
101 }
102
103 if let Some(ref url) = self.url {
104 parts.push(format!("[redirect: {}]", url));
105 }
106
107 parts.push(format!("[phase: {}]", self.phase.name()));
108
109 parts.join(" ")
110 }
111}
112
113impl Default for Intervention {
114 fn default() -> Self {
115 Self::new(403, Phase::RequestHeaders)
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct InterventionBuilder {
122 intervention: Intervention,
123}
124
125impl InterventionBuilder {
126 pub fn new() -> Self {
128 Self {
129 intervention: Intervention::default(),
130 }
131 }
132
133 pub fn status(mut self, status: u16) -> Self {
135 self.intervention.status = status;
136 self
137 }
138
139 pub fn phase(mut self, phase: Phase) -> Self {
141 self.intervention.phase = phase;
142 self
143 }
144
145 pub fn redirect(mut self, url: String) -> Self {
147 self.intervention.status = 302;
148 self.intervention.url = Some(url);
149 self
150 }
151
152 pub fn drop_connection(mut self) -> Self {
154 self.intervention.drop_connection = true;
155 self.intervention.status = 444;
156 self
157 }
158
159 pub fn rule_id(mut self, id: String) -> Self {
161 self.intervention.rule_ids.push(id);
162 self
163 }
164
165 pub fn log(mut self, msg: String) -> Self {
167 self.intervention.log = Some(msg);
168 self
169 }
170
171 pub fn build(self) -> Intervention {
173 self.intervention
174 }
175}
176
177impl Default for InterventionBuilder {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_deny_intervention() {
189 let intervention = Intervention::deny(403, Phase::RequestHeaders, Some("12345".to_string()));
190 assert_eq!(intervention.status, 403);
191 assert_eq!(intervention.rule_ids, vec!["12345".to_string()]);
192 assert!(!intervention.drop_connection);
193 }
194
195 #[test]
196 fn test_redirect_intervention() {
197 let intervention = Intervention::redirect(
198 "https://example.com/blocked".to_string(),
199 Phase::RequestHeaders,
200 Some("12345".to_string()),
201 );
202 assert_eq!(intervention.status, 302);
203 assert_eq!(
204 intervention.url,
205 Some("https://example.com/blocked".to_string())
206 );
207 }
208
209 #[test]
210 fn test_builder() {
211 let intervention = InterventionBuilder::new()
212 .status(403)
213 .phase(Phase::RequestBody)
214 .rule_id("100".to_string())
215 .log("SQL Injection detected".to_string())
216 .build();
217
218 assert_eq!(intervention.status, 403);
219 assert_eq!(intervention.phase, Phase::RequestBody);
220 assert_eq!(intervention.rule_ids, vec!["100".to_string()]);
221 }
222
223 #[test]
224 fn test_format_log() {
225 let intervention = Intervention::deny(403, Phase::RequestHeaders, Some("942100".to_string()));
226 let log = intervention.format_log();
227 assert!(log.contains("[status 403]"));
228 assert!(log.contains("942100"));
229 }
230}