agentsight_capture/analyzers/
ssl_filter.rs1use super::common;
5use super::filter_base::{FilterBase, FilterExpr, MetricsStrategy};
6use super::{Analyzer, AnalyzerError};
7use crate::runners::EventStream;
8use async_trait::async_trait;
9use serde_json::Value;
10
11static GLOBAL_METRICS: super::filter_metrics::MetricsSlot = std::sync::OnceLock::new();
12
13pub fn print_global_ssl_filter_metrics() {
14 super::filter_metrics::print("SSLFilter", &GLOBAL_METRICS);
15}
16
17pub struct SSLFilter {
18 base: FilterBase<SslFilterExpr>,
19}
20
21#[derive(Debug, Clone)]
22pub struct SslFilterExpr {
23 parsed: FilterNode,
24}
25
26#[derive(Debug, Clone)]
27enum FilterNode {
28 And(Box<FilterNode>, Box<FilterNode>),
29 Or(Box<FilterNode>, Box<FilterNode>),
30 Condition {
31 field: String,
32 operator: String,
33 value: String,
34 },
35 Empty,
36}
37
38impl FilterExpr for SslFilterExpr {
39 fn evaluate(&self, data: &Value) -> bool {
40 evaluate_node(&self.parsed, data)
41 }
42}
43
44impl SSLFilter {
45 pub fn with_patterns(patterns: Vec<String>) -> Self {
46 Self {
47 base: FilterBase::new("ssl", MetricsStrategy::AddOnDrop, &GLOBAL_METRICS)
48 .with_patterns(patterns, SslFilterExpr::parse),
49 }
50 }
51}
52
53#[async_trait]
54impl Analyzer for SSLFilter {
55 async fn process(&mut self, stream: EventStream) -> Result<EventStream, AnalyzerError> {
56 self.base.process(stream).await
57 }
58}
59
60impl SslFilterExpr {
63 pub fn parse(expression: &str) -> Self {
64 Self {
65 parsed: parse_expression(expression),
66 }
67 }
68
69 pub fn process_escape_sequences(value: &str) -> String {
70 let mut result = String::new();
71 let mut chars = value.chars().peekable();
72 while let Some(ch) = chars.next() {
73 if ch == '\\' {
74 match chars.peek() {
75 Some('r') => {
76 chars.next();
77 result.push('\r');
78 }
79 Some('n') => {
80 chars.next();
81 result.push('\n');
82 }
83 Some('t') => {
84 chars.next();
85 result.push('\t');
86 }
87 Some('\\') => {
88 chars.next();
89 result.push('\\');
90 }
91 Some('"') => {
92 chars.next();
93 result.push('"');
94 }
95 _ => result.push(ch),
96 }
97 } else {
98 result.push(ch);
99 }
100 }
101 result
102 }
103}
104
105fn parse_expression(expr: &str) -> FilterNode {
106 let expr = expr.trim();
107 if expr.is_empty() {
108 return FilterNode::Empty;
109 }
110 if let Some(pos) = find_operator(expr, '|') {
111 return FilterNode::Or(
112 Box::new(parse_expression(&expr[..pos])),
113 Box::new(parse_expression(&expr[pos + 1..])),
114 );
115 }
116 if let Some(pos) = find_operator(expr, '&') {
117 return FilterNode::And(
118 Box::new(parse_expression(&expr[..pos])),
119 Box::new(parse_expression(&expr[pos + 1..])),
120 );
121 }
122 parse_condition(expr)
123}
124
125fn find_operator(expr: &str, op: char) -> Option<usize> {
126 let mut depth = 0;
127 for (i, c) in expr.chars().enumerate() {
128 match c {
129 '(' => depth += 1,
130 ')' => depth -= 1,
131 _ if c == op && depth == 0 => return Some(i),
132 _ => {}
133 }
134 }
135 None
136}
137
138fn parse_condition(expr: &str) -> FilterNode {
139 let expr = expr.trim();
140 if expr.starts_with('(') && expr.ends_with(')') {
141 return parse_expression(&expr[1..expr.len() - 1]);
142 }
143 for &op in &[">=", "<=", "!=", "=", ">", "<", "~"] {
144 if let Some(pos) = expr.find(op) {
145 let field = expr[..pos].trim().to_string();
146 let raw_value = expr[pos + op.len()..].trim();
147 let operator = match op {
148 "=" => "exact",
149 "!=" => "not_equal",
150 ">" => "gt",
151 "<" => "lt",
152 ">=" => "gte",
153 "<=" => "lte",
154 "~" => "contains",
155 _ => "exact",
156 }
157 .to_string();
158 return FilterNode::Condition {
159 field,
160 operator,
161 value: SslFilterExpr::process_escape_sequences(raw_value),
162 };
163 }
164 }
165 FilterNode::Empty
166}
167
168fn evaluate_node(node: &FilterNode, data: &Value) -> bool {
171 match node {
172 FilterNode::And(l, r) => evaluate_node(l, data) && evaluate_node(r, data),
173 FilterNode::Or(l, r) => evaluate_node(l, data) || evaluate_node(r, data),
174 FilterNode::Condition {
175 field,
176 operator,
177 value,
178 } => eval_condition(field, operator, value, data),
179 FilterNode::Empty => false,
180 }
181}
182
183fn eval_condition(field: &str, operator: &str, expected: &str, data: &Value) -> bool {
184 if field == "data.type" {
185 if let Some(v) = data.get("data").and_then(|v| v.as_str()) {
186 return cmp_str(common::detect_data_type(v), operator, expected);
187 }
188 return false;
189 }
190 match field {
191 "is_handshake" | "truncated" => {
192 data.get(field).and_then(|v| v.as_bool()).unwrap_or(false) == (expected == "true")
193 }
194 "len" | "pid" | "tid" | "uid" | "timestamp_ns" => data
195 .get(field)
196 .and_then(|v| v.as_u64())
197 .map(|n| cmp_num(n, operator, expected))
198 .unwrap_or(false),
199 "latency_ms" => data
200 .get("latency_ms")
201 .and_then(|v| v.as_f64())
202 .map(|n| cmp_float(n, operator, expected))
203 .unwrap_or(false),
204 _ => data
205 .get(field)
206 .and_then(|v| v.as_str())
207 .map(|v| cmp_str(v, operator, expected))
208 .unwrap_or(false),
209 }
210}
211
212fn cmp_str(actual: &str, op: &str, expected: &str) -> bool {
213 match op {
214 "exact" => actual == expected,
215 "not_equal" => actual != expected,
216 "contains" => actual.contains(expected),
217 "prefix" => actual.starts_with(expected),
218 "suffix" => actual.ends_with(expected),
219 _ => false,
220 }
221}
222
223fn cmp_num(actual: u64, op: &str, expected: &str) -> bool {
224 let Ok(e) = expected.parse::<u64>() else {
225 return false;
226 };
227 match op {
228 "exact" => actual == e,
229 "not_equal" => actual != e,
230 "gt" => actual > e,
231 "lt" => actual < e,
232 "gte" => actual >= e,
233 "lte" => actual <= e,
234 _ => false,
235 }
236}
237
238fn cmp_float(actual: f64, op: &str, expected: &str) -> bool {
239 let Ok(e) = expected.parse::<f64>() else {
240 return false;
241 };
242 match op {
243 "exact" => (actual - e).abs() < f64::EPSILON,
244 "not_equal" => (actual - e).abs() >= f64::EPSILON,
245 "gt" => actual > e,
246 "lt" => actual < e,
247 "gte" => actual >= e,
248 "lte" => actual <= e,
249 _ => false,
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use serde_json::json;
257
258 #[test]
259 fn test_expression_parsing() {
260 let expr = SslFilterExpr::parse("function=READ/RECV");
261 assert!(expr.evaluate(&json!({"function": "READ/RECV"})));
262 assert!(!expr.evaluate(&json!({"function": "WRITE/SEND"})));
263 }
264
265 #[test]
266 fn test_data_filtering() {
267 let expr = SslFilterExpr::parse("data~chunked");
268 assert!(expr.evaluate(&json!({"data": "chunked data here"})));
269 assert!(!expr.evaluate(&json!({"data": "plain text"})));
270 }
271
272 #[test]
273 fn test_numeric_filtering() {
274 let expr = SslFilterExpr::parse("len<10");
275 assert!(expr.evaluate(&json!({"len": 5})));
276 assert!(!expr.evaluate(&json!({"len": 15})));
277 }
278
279 #[test]
280 fn test_complex_expressions() {
281 let expr = SslFilterExpr::parse("data~chunked&function=READ/RECV");
282 assert!(expr.evaluate(&json!({"data": "chunked data", "function": "READ/RECV"})));
283 assert!(!expr.evaluate(&json!({"data": "chunked data", "function": "WRITE/SEND"})));
284 assert!(!expr.evaluate(&json!({"data": "plain text", "function": "WRITE/SEND"})));
285 }
286
287 #[test]
288 fn test_escape_sequences() {
289 assert_eq!(
290 SslFilterExpr::process_escape_sequences("0\\r\\n\\r\\n"),
291 "0\r\n\r\n"
292 );
293 assert_eq!(
294 SslFilterExpr::process_escape_sequences("hello\\tworld\\n"),
295 "hello\tworld\n"
296 );
297
298 let expr = SslFilterExpr::parse("data=0\\r\\n\\r\\n");
299 assert!(expr.evaluate(&json!({"data": "0\r\n\r\n"})));
300 assert!(!expr.evaluate(&json!({"data": "HTTP/1.1 200 OK"})));
301 }
302}