Skip to main content

agentsight_capture/analyzers/
ssl_filter.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 eunomia-bpf org.
3
4use 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
60// --- Expression parsing ---
61
62impl 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
168// --- Evaluation ---
169
170fn 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}