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 NOSQL_RULES: &[Rule] = &[
23 Rule {
24 id: "nosql-where-js",
25 pattern: r"\$where\b",
27 severity: Severity::Critical,
28 paranoia: 1,
29 },
30 Rule {
31 id: "nosql-shell-method",
32 pattern: r"\bdb\.\w+\.(?:insert|find|update|remove|save|drop|count|aggregate)\s*\(",
34 severity: Severity::Critical,
35 paranoia: 1,
36 },
37 Rule {
38 id: "nosql-js-timing",
39 pattern: r"do\s*\{[^}]*\}\s*while\s*\(",
41 severity: Severity::Warning,
42 paranoia: 2,
43 },
44 Rule {
45 id: "nosql-operator",
46 pattern: r"\$(?:or|and|nor|ne|gt|gte|lt|lte|nin|in|regex|exists|expr|elemMatch)\b",
49 severity: Severity::Warning,
50 paranoia: 2,
51 },
52];
53
54#[derive(Default)]
57pub struct NosqlModule {
58 rule_set: Option<RegexSet>,
59 active_rules: Vec<&'static Rule>,
60}
61
62impl NosqlModule {
63 pub fn new() -> Self {
64 Self::default()
65 }
66}
67
68impl WafModule for NosqlModule {
69 fn id(&self) -> &str {
70 "nosql"
71 }
72
73 fn phase(&self) -> Phase {
74 Phase::Body
75 }
76
77 fn init(&mut self, cfg: &Config) {
78 let pl = cfg.waf.paranoia_level;
79 self.active_rules = NOSQL_RULES.iter().filter(|r| r.paranoia <= pl).collect();
80 self.rule_set = Some(
81 RegexSet::new(self.active_rules.iter().map(|r| r.pattern))
82 .expect("NoSQL rule compilation failed — check patterns at startup"),
83 );
84 }
85
86 fn inspect(&self, ctx: &RequestContext) -> Decision {
87 let Some(rule_set) = &self.rule_set else {
88 return Decision::Allow;
89 };
90
91 let query = ctx.normalized.query_params.iter().map(|(_, v)| v.as_str());
92 let cookies = ctx.normalized.cookies.iter().map(|(_, v)| v.as_str());
93 let body_vals = body_str_values(&ctx.normalized.body);
94 let body = body_vals.iter().map(String::as_str);
95 let derived = ctx.normalized.derived_decoded.iter().map(String::as_str);
96
97 let path = std::iter::once(ctx.normalized.path.as_str());
101 let headers = inspectable_header_values(ctx);
103 let matched = all_matches(rule_set, path.chain(query).chain(cookies).chain(body).chain(derived).chain(headers));
104 if matched.is_empty() {
105 return Decision::Allow;
106 }
107
108 let items: Vec<ScoreItem> = matched
109 .iter()
110 .map(|&idx| {
111 let rule = self.active_rules[idx];
112 warn!(
113 request_id = %ctx.request_id,
114 rule_id = %rule.id,
115 severity = ?rule.severity,
116 "nosql detection"
117 );
118 ScoreItem {
119 rule_id: rule.id.to_string(),
120 severity: rule.severity,
121 }
122 })
123 .collect();
124
125 Decision::Scores(items)
126 }
127}