1use regex::RegexSet;
5use tracing::warn;
6use waf_core::{Config, Decision, Phase, RequestContext, ScoreItem, Severity, WafModule};
7
8use crate::{all_matches, body_str_values, inspectable_header_values, Rule};
9
10pub static MAIL_RULES: &[Rule] = &[
25 Rule {
26 id: "mail-command-injection",
27 pattern: r"(?i)[\r\n]\s*(?:\w+\s+)?(?:CAPABILITY|FETCH|QUIT|RCPT\s+TO|MAIL\s+FROM|STARTTLS|EHLO|HELO)\b",
29 severity: Severity::Critical,
30 paranoia: 1,
31 },
32];
33
34#[derive(Default)]
37pub struct MailModule {
38 rule_set: Option<RegexSet>,
39 active_rules: Vec<&'static Rule>,
40}
41
42impl MailModule {
43 pub fn new() -> Self {
44 Self::default()
45 }
46}
47
48impl WafModule for MailModule {
49 fn id(&self) -> &str {
50 "mail"
51 }
52
53 fn phase(&self) -> Phase {
54 Phase::Body
55 }
56
57 fn init(&mut self, cfg: &Config) {
58 let pl = cfg.waf.paranoia_level;
59 self.active_rules = MAIL_RULES.iter().filter(|r| r.paranoia <= pl).collect();
60 self.rule_set = Some(
61 RegexSet::new(self.active_rules.iter().map(|r| r.pattern))
62 .expect("Mail rule compilation failed — check patterns at startup"),
63 );
64 }
65
66 fn inspect(&self, ctx: &RequestContext) -> Decision {
67 let Some(rule_set) = &self.rule_set else {
68 return Decision::Allow;
69 };
70
71 let query = ctx.normalized.query_params.iter().map(|(_, v)| v.as_str());
72 let cookies = ctx.normalized.cookies.iter().map(|(_, v)| v.as_str());
73 let body_vals = body_str_values(&ctx.normalized.body);
74 let body = body_vals.iter().map(String::as_str);
75 let derived = ctx.normalized.derived_decoded.iter().map(String::as_str);
76
77 let path = std::iter::once(ctx.normalized.path.as_str());
81 let headers = inspectable_header_values(ctx);
83 let matched = all_matches(rule_set, path.chain(query).chain(cookies).chain(body).chain(derived).chain(headers));
84 if matched.is_empty() {
85 return Decision::Allow;
86 }
87
88 let items: Vec<ScoreItem> = matched
89 .iter()
90 .map(|&idx| {
91 let rule = self.active_rules[idx];
92 warn!(
93 request_id = %ctx.request_id,
94 rule_id = %rule.id,
95 severity = ?rule.severity,
96 "mail detection"
97 );
98 ScoreItem {
99 rule_id: rule.id.to_string(),
100 severity: rule.severity,
101 }
102 })
103 .collect();
104
105 Decision::Scores(items)
106 }
107}