sentinel_modsec/variables/
resolver.rs

1//! Variable resolution engine.
2
3use super::{RequestData, ResponseData, TxCollection};
4use crate::parser::{Selection, VariableName, VariableSpec};
5use regex::Regex;
6
7/// Variable resolver for transaction context.
8pub struct VariableResolver<'a> {
9    request: &'a RequestData,
10    response: &'a ResponseData,
11    tx: &'a TxCollection,
12    matched_var: Option<&'a str>,
13    matched_vars: &'a [(String, String)],
14    captures: &'a [String],
15}
16
17impl<'a> VariableResolver<'a> {
18    /// Create a new resolver.
19    pub fn new(
20        request: &'a RequestData,
21        response: &'a ResponseData,
22        tx: &'a TxCollection,
23        matched_var: Option<&'a str>,
24        matched_vars: &'a [(String, String)],
25        captures: &'a [String],
26    ) -> Self {
27        Self {
28            request,
29            response,
30            tx,
31            matched_var,
32            matched_vars,
33            captures,
34        }
35    }
36
37    /// Resolve a variable specification to values.
38    pub fn resolve(&self, spec: &VariableSpec) -> Vec<(String, String)> {
39        let values = self.resolve_variable(spec.name, &spec.selection);
40
41        // Apply exclusions
42        if spec.exclusions.is_empty() {
43            values
44        } else {
45            values
46                .into_iter()
47                .filter(|(k, _)| !spec.exclusions.iter().any(|e| k.contains(e)))
48                .collect()
49        }
50    }
51
52    /// Resolve a variable by name.
53    fn resolve_variable(
54        &self,
55        name: VariableName,
56        selection: &Option<Selection>,
57    ) -> Vec<(String, String)> {
58        match name {
59            // Request variables
60            VariableName::RequestUri => {
61                vec![("REQUEST_URI".to_string(), self.request.uri.clone())]
62            }
63            VariableName::RequestUriRaw => {
64                vec![("REQUEST_URI_RAW".to_string(), self.request.uri_raw.clone())]
65            }
66            VariableName::RequestMethod => {
67                vec![("REQUEST_METHOD".to_string(), self.request.method.clone())]
68            }
69            VariableName::RequestProtocol => {
70                vec![(
71                    "REQUEST_PROTOCOL".to_string(),
72                    self.request.protocol.clone(),
73                )]
74            }
75            VariableName::QueryString => {
76                vec![(
77                    "QUERY_STRING".to_string(),
78                    self.request.query_string.clone(),
79                )]
80            }
81            VariableName::RequestFilename => {
82                vec![("REQUEST_FILENAME".to_string(), self.request.path.clone())]
83            }
84            VariableName::RequestBody => {
85                vec![("REQUEST_BODY".to_string(), self.request.body_str())]
86            }
87            VariableName::RequestBodyLength => {
88                vec![(
89                    "REQUEST_BODY_LENGTH".to_string(),
90                    self.request.body_length().to_string(),
91                )]
92            }
93
94            // Collections
95            VariableName::Args => self.resolve_collection_from_all_args(selection),
96            VariableName::ArgsGet => {
97                self.resolve_collection(&self.request.args_get, "ARGS_GET", selection)
98            }
99            VariableName::ArgsPost => {
100                self.resolve_collection(&self.request.args_post, "ARGS_POST", selection)
101            }
102            VariableName::RequestHeaders => {
103                self.resolve_collection(&self.request.headers, "REQUEST_HEADERS", selection)
104            }
105            VariableName::RequestCookies => {
106                self.resolve_collection(&self.request.cookies, "REQUEST_COOKIES", selection)
107            }
108
109            // Response variables
110            VariableName::ResponseStatus => {
111                vec![(
112                    "RESPONSE_STATUS".to_string(),
113                    self.response.status.to_string(),
114                )]
115            }
116            VariableName::ResponseBody => {
117                vec![("RESPONSE_BODY".to_string(), self.response.body_str())]
118            }
119            VariableName::ResponseContentType => {
120                vec![(
121                    "RESPONSE_CONTENT_TYPE".to_string(),
122                    self.response.content_type.clone(),
123                )]
124            }
125            VariableName::ResponseHeaders => {
126                self.resolve_collection(&self.response.headers, "RESPONSE_HEADERS", selection)
127            }
128
129            // TX collection
130            VariableName::Tx => self.resolve_tx_collection(selection),
131
132            // Client/Server info
133            VariableName::RemoteAddr => {
134                vec![("REMOTE_ADDR".to_string(), self.request.client_ip.clone())]
135            }
136            VariableName::RemotePort => {
137                vec![(
138                    "REMOTE_PORT".to_string(),
139                    self.request.client_port.to_string(),
140                )]
141            }
142            VariableName::ServerName => {
143                vec![("SERVER_NAME".to_string(), self.request.server_name.clone())]
144            }
145            VariableName::ServerPort => {
146                vec![(
147                    "SERVER_PORT".to_string(),
148                    self.request.server_port.to_string(),
149                )]
150            }
151
152            // Matched variables
153            VariableName::MatchedVar => {
154                if let Some(v) = self.matched_var {
155                    vec![("MATCHED_VAR".to_string(), v.to_string())]
156                } else {
157                    vec![]
158                }
159            }
160            VariableName::MatchedVars => self
161                .matched_vars
162                .iter()
163                .map(|(k, v)| (format!("MATCHED_VARS:{}", k), v.clone()))
164                .collect(),
165
166            // Default - empty
167            _ => vec![],
168        }
169    }
170
171    /// Resolve a collection with optional selection.
172    fn resolve_collection(
173        &self,
174        collection: &super::collection::HashMapCollection,
175        prefix: &str,
176        selection: &Option<Selection>,
177    ) -> Vec<(String, String)> {
178        use super::collection::Collection;
179
180        match selection {
181            Some(Selection::Key(key)) => {
182                if let Some(values) = collection.get(key) {
183                    values
184                        .into_iter()
185                        .map(|v| (format!("{}:{}", prefix, key), v.to_string()))
186                        .collect()
187                } else {
188                    vec![]
189                }
190            }
191            Some(Selection::Regex(pattern)) => {
192                if let Ok(re) = Regex::new(pattern) {
193                    collection
194                        .get_regex(&re)
195                        .into_iter()
196                        .map(|(k, v)| (format!("{}:{}", prefix, k), v.to_string()))
197                        .collect()
198                } else {
199                    vec![]
200                }
201            }
202            None => collection
203                .all()
204                .into_iter()
205                .map(|(k, v)| (format!("{}:{}", prefix, k), v.to_string()))
206                .collect(),
207        }
208    }
209
210    /// Resolve ARGS collection (GET + POST combined).
211    fn resolve_collection_from_all_args(&self, selection: &Option<Selection>) -> Vec<(String, String)> {
212        let mut result = self.resolve_collection(&self.request.args_get, "ARGS", selection);
213        result.extend(self.resolve_collection(&self.request.args_post, "ARGS", selection));
214        result
215    }
216
217    /// Resolve TX collection.
218    fn resolve_tx_collection(&self, selection: &Option<Selection>) -> Vec<(String, String)> {
219        use super::collection::Collection;
220
221        match selection {
222            Some(Selection::Key(key)) => {
223                if let Some(values) = self.tx.get(key) {
224                    values
225                        .into_iter()
226                        .map(|v| (format!("TX:{}", key), v.to_string()))
227                        .collect()
228                } else {
229                    vec![]
230                }
231            }
232            Some(Selection::Regex(pattern)) => {
233                if let Ok(re) = Regex::new(pattern) {
234                    self.tx
235                        .get_regex(&re)
236                        .into_iter()
237                        .map(|(k, v)| (format!("TX:{}", k), v.to_string()))
238                        .collect()
239                } else {
240                    vec![]
241                }
242            }
243            None => self
244                .tx
245                .all()
246                .into_iter()
247                .map(|(k, v)| (format!("TX:{}", k), v.to_string()))
248                .collect(),
249        }
250    }
251}