Skip to main content

agentsight_capture/analyzers/
http_filter.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 eunomia-bpf org.
3
4use super::filter_base::{FilterBase, FilterExpr, MetricsStrategy};
5use super::{Analyzer, AnalyzerError};
6use crate::runners::EventStream;
7use async_trait::async_trait;
8use serde_json::Value;
9
10static GLOBAL_METRICS: super::filter_metrics::MetricsSlot = std::sync::OnceLock::new();
11
12pub fn print_global_http_filter_metrics() {
13    super::filter_metrics::print("HTTPFilter", &GLOBAL_METRICS);
14}
15
16pub struct HTTPFilter {
17    base: FilterBase<HttpFilterExpr>,
18}
19
20#[derive(Debug, Clone)]
21pub struct HttpFilterExpr {
22    parsed: FilterNode,
23}
24
25#[derive(Debug, Clone)]
26enum FilterNode {
27    And(Vec<FilterNode>),
28    Or(Vec<FilterNode>),
29    Condition {
30        target: String,
31        field: String,
32        operator: String,
33        value: String,
34    },
35    Empty,
36}
37
38impl FilterExpr for HttpFilterExpr {
39    fn evaluate(&self, data: &Value) -> bool {
40        evaluate_node(&self.parsed, data)
41    }
42}
43
44impl HTTPFilter {
45    pub fn with_patterns(patterns: Vec<String>) -> Self {
46        Self {
47            base: FilterBase::new("http_parser", MetricsStrategy::SetPerEvent, &GLOBAL_METRICS)
48                .with_patterns(patterns, HttpFilterExpr::parse),
49        }
50    }
51}
52
53#[async_trait]
54impl Analyzer for HTTPFilter {
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 HttpFilterExpr {
63    pub fn parse(expression: &str) -> Self {
64        let trimmed = expression.trim();
65        if trimmed.is_empty() {
66            return Self {
67                parsed: FilterNode::Empty,
68            };
69        }
70        Self {
71            parsed: parse_or(trimmed),
72        }
73    }
74}
75
76fn parse_or(expr: &str) -> FilterNode {
77    let parts: Vec<&str> = expr.split('|').map(str::trim).collect();
78    if parts.len() > 1 {
79        FilterNode::Or(parts.into_iter().map(parse_and).collect())
80    } else {
81        parse_and(expr)
82    }
83}
84
85fn parse_and(expr: &str) -> FilterNode {
86    let parts: Vec<&str> = expr.split('&').map(str::trim).collect();
87    if parts.len() > 1 {
88        FilterNode::And(parts.into_iter().map(parse_single).collect())
89    } else {
90        parse_single(expr)
91    }
92}
93
94fn parse_single(cond: &str) -> FilterNode {
95    let cond = cond.trim();
96    if !cond.contains('=') {
97        return FilterNode::Condition {
98            target: "request".into(),
99            field: "path".into(),
100            operator: "contains".into(),
101            value: cond.into(),
102        };
103    }
104    let Some((key, value)) = cond.split_once('=') else {
105        return FilterNode::Empty;
106    };
107    let (key, value) = (key.trim(), value.trim());
108
109    if let Some((target_raw, field)) = key.split_once('.') {
110        let (target, operator) = match target_raw.trim() {
111            "request" | "req" => {
112                let op = match field.trim() {
113                    "path_prefix" | "path_starts_with" => "prefix",
114                    "path_contains" | "path_includes" => "contains",
115                    _ => "exact",
116                };
117                ("request", op)
118            }
119            "response" | "resp" | "res" => ("response", "exact"),
120            _ => ("request", "exact"),
121        };
122        FilterNode::Condition {
123            target: target.into(),
124            field: field.trim().into(),
125            operator: operator.into(),
126            value: value.into(),
127        }
128    } else {
129        let operator = match key {
130            "path_prefix" | "path_starts_with" => "prefix",
131            "path_contains" | "path_includes" => "contains",
132            _ => "exact",
133        };
134        FilterNode::Condition {
135            target: "request".into(),
136            field: key.into(),
137            operator: operator.into(),
138            value: value.into(),
139        }
140    }
141}
142
143// --- Evaluation ---
144
145fn evaluate_node(node: &FilterNode, data: &Value) -> bool {
146    match node {
147        FilterNode::Empty => false,
148        FilterNode::And(cs) => cs.iter().all(|c| evaluate_node(c, data)),
149        FilterNode::Or(cs) => cs.iter().any(|c| evaluate_node(c, data)),
150        FilterNode::Condition {
151            target,
152            field,
153            operator,
154            value,
155        } => {
156            let msg_type = data
157                .get("message_type")
158                .and_then(|v| v.as_str())
159                .unwrap_or("");
160            let matches_target = match target.as_str() {
161                "request" => msg_type == "request",
162                "response" => msg_type == "response",
163                _ => false,
164            };
165            if !matches_target {
166                return false;
167            }
168            if target == "request" {
169                eval_request(field, operator, value, data)
170            } else {
171                eval_response(field, value, data)
172            }
173        }
174    }
175}
176
177fn eval_request(field: &str, operator: &str, value: &str, data: &Value) -> bool {
178    match field {
179        "method" | "verb" => {
180            let m = data.get("method").and_then(|v| v.as_str()).unwrap_or("");
181            m.eq_ignore_ascii_case(value)
182        }
183        "path" | "path_exact" => {
184            let p = data.get("path").and_then(|v| v.as_str()).unwrap_or("");
185            match operator {
186                "prefix" => p.starts_with(value),
187                "contains" => p.contains(value),
188                _ => p == value,
189            }
190        }
191        "path_prefix" | "path_starts_with" => data
192            .get("path")
193            .and_then(|v| v.as_str())
194            .unwrap_or("")
195            .starts_with(value),
196        "path_contains" | "path_includes" => data
197            .get("path")
198            .and_then(|v| v.as_str())
199            .unwrap_or("")
200            .contains(value),
201        "host" | "hostname" => header_val(data, "host").unwrap_or("") == value,
202        "body" | "body_contains" => data
203            .get("body")
204            .and_then(|v| v.as_str())
205            .unwrap_or("")
206            .contains(value),
207        _ => {
208            let path = data.get("path").and_then(|v| v.as_str()).unwrap_or("");
209            path.split_once('?')
210                .map(|(_, q)| q.contains(&format!("{field}={value}")))
211                .unwrap_or(false)
212        }
213    }
214}
215
216fn eval_response(field: &str, value: &str, data: &Value) -> bool {
217    match field {
218        "status_code" | "status" | "code" => {
219            let sc = data
220                .get("status_code")
221                .and_then(|v| v.as_u64())
222                .unwrap_or(0);
223            value.parse::<u64>().ok().is_some_and(|v| sc == v)
224        }
225        "status_text" | "status_message" => {
226            let t = data
227                .get("status_text")
228                .and_then(|v| v.as_str())
229                .unwrap_or("");
230            t.to_lowercase().contains(&value.to_lowercase())
231        }
232        "content_type" | "content-type" => header_val(data, "content-type")
233            .unwrap_or("")
234            .contains(value),
235        "body" | "body_contains" => data
236            .get("body")
237            .and_then(|v| v.as_str())
238            .unwrap_or("")
239            .contains(value),
240        _ => header_val(data, field).unwrap_or("").contains(value),
241    }
242}
243
244fn header_val<'a>(data: &'a Value, name: &str) -> Option<&'a str> {
245    data.get("headers")?.get(name)?.as_str()
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use serde_json::json;
252
253    #[test]
254    fn test_expression_parsing() {
255        let expr = HttpFilterExpr::parse("request.path=/health");
256        assert!(expr.evaluate(&json!({"message_type": "request", "path": "/health"})));
257        assert!(!expr.evaluate(&json!({"message_type": "request", "path": "/api"})));
258    }
259
260    #[test]
261    fn test_request_filtering() {
262        let f = HttpFilterExpr::parse("request.method=GET");
263        assert!(f.evaluate(&json!({"message_type": "request", "method": "GET"})));
264        assert!(!f.evaluate(&json!({"message_type": "request", "method": "POST"})));
265    }
266
267    #[test]
268    fn test_response_filtering() {
269        let f = HttpFilterExpr::parse("response.status_code=404");
270        assert!(f.evaluate(&json!({"message_type": "response", "status_code": 404})));
271        assert!(!f.evaluate(&json!({"message_type": "response", "status_code": 200})));
272    }
273
274    #[test]
275    fn test_complex_expressions() {
276        let f = HttpFilterExpr::parse("request.method=GET | response.status_code=404");
277        assert!(f.evaluate(&json!({"message_type": "request", "method": "GET"})));
278        assert!(f.evaluate(&json!({"message_type": "response", "status_code": 404})));
279        assert!(!f.evaluate(&json!({"message_type": "request", "method": "POST"})));
280    }
281}