1use chrono::{DateTime, Utc};
8
9use crate::entry::{AuditEntry, EventSeverity};
10
11#[derive(Debug, Clone, Default)]
24pub struct QueryFilter {
25 pub(crate) source: Option<String>,
26 pub(crate) severity: Option<EventSeverity>,
27 pub(crate) agent_id: Option<String>,
28 pub(crate) after: Option<DateTime<Utc>>,
29 pub(crate) before: Option<DateTime<Utc>>,
30 pub(crate) action: Option<String>,
31 pub(crate) min_severity: Option<EventSeverity>,
32}
33
34impl QueryFilter {
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn source(mut self, source: impl Into<String>) -> Self {
41 self.source = Some(source.into());
42 self
43 }
44
45 pub fn severity(mut self, severity: EventSeverity) -> Self {
47 self.severity = Some(severity);
48 self
49 }
50
51 pub fn agent_id(mut self, agent_id: impl Into<String>) -> Self {
53 self.agent_id = Some(agent_id.into());
54 self
55 }
56
57 pub fn after(mut self, after: DateTime<Utc>) -> Self {
59 self.after = Some(after);
60 self
61 }
62
63 pub fn before(mut self, before: DateTime<Utc>) -> Self {
65 self.before = Some(before);
66 self
67 }
68
69 pub fn min_severity(mut self, min: EventSeverity) -> Self {
71 self.min_severity = Some(min);
72 self
73 }
74
75 pub fn action(mut self, action: impl Into<String>) -> Self {
77 self.action = Some(action.into());
78 self
79 }
80
81 pub fn matches(&self, entry: &AuditEntry) -> bool {
83 if let Some(ref s) = self.source
84 && entry.source() != s
85 {
86 return false;
87 }
88 if let Some(sev) = self.severity
89 && entry.severity() != sev
90 {
91 return false;
92 }
93 if let Some(ref a) = self.agent_id
94 && entry.agent_id() != Some(a.as_str())
95 {
96 return false;
97 }
98 if let Some(ref a) = self.action
99 && entry.action() != a
100 {
101 return false;
102 }
103 if let Some(min) = self.min_severity
104 && entry.severity() < min
105 {
106 return false;
107 }
108 if let Some(after) = self.after
109 && entry.timestamp() <= after
110 {
111 return false;
112 }
113 if let Some(before) = self.before
114 && entry.timestamp() >= before
115 {
116 return false;
117 }
118 true
119 }
120
121 pub fn apply<'a>(&self, entries: &'a [AuditEntry]) -> Vec<&'a AuditEntry> {
123 entries.iter().filter(|e| self.matches(e)).collect()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn make_chain() -> Vec<AuditEntry> {
132 let e1 = AuditEntry::new(
133 EventSeverity::Info,
134 "daimon",
135 "agent.start",
136 serde_json::json!({}),
137 "",
138 )
139 .with_agent("agent-01");
140 let e2 = AuditEntry::new(
141 EventSeverity::Security,
142 "aegis",
143 "alert",
144 serde_json::json!({}),
145 e1.hash(),
146 )
147 .with_agent("agent-01");
148 let e3 = AuditEntry::new(
149 EventSeverity::Info,
150 "daimon",
151 "agent.stop",
152 serde_json::json!({}),
153 e2.hash(),
154 )
155 .with_agent("agent-02");
156 vec![e1, e2, e3]
157 }
158
159 #[test]
160 fn filter_by_source() {
161 let entries = make_chain();
162 let results = QueryFilter::new().source("daimon").apply(&entries);
163 assert_eq!(results.len(), 2);
164 }
165
166 #[test]
167 fn filter_by_severity() {
168 let entries = make_chain();
169 let results = QueryFilter::new()
170 .severity(EventSeverity::Security)
171 .apply(&entries);
172 assert_eq!(results.len(), 1);
173 assert_eq!(results[0].source(), "aegis");
174 }
175
176 #[test]
177 fn filter_by_agent() {
178 let entries = make_chain();
179 let results = QueryFilter::new().agent_id("agent-01").apply(&entries);
180 assert_eq!(results.len(), 2);
181 }
182
183 #[test]
184 fn filter_by_action() {
185 let entries = make_chain();
186 let results = QueryFilter::new().action("alert").apply(&entries);
187 assert_eq!(results.len(), 1);
188 }
189
190 #[test]
191 fn filter_combined() {
192 let entries = make_chain();
193 let results = QueryFilter::new()
194 .source("daimon")
195 .agent_id("agent-01")
196 .apply(&entries);
197 assert_eq!(results.len(), 1);
198 assert_eq!(results[0].action(), "agent.start");
199 }
200
201 #[test]
202 fn filter_by_time_range() {
203 let entries = make_chain();
204 let after = entries[0].timestamp();
206 let results = QueryFilter::new().after(after).apply(&entries);
207 assert!(results.iter().all(|e| e.timestamp() > after));
209 }
210
211 #[test]
212 fn filter_no_criteria_matches_all() {
213 let entries = make_chain();
214 let results = QueryFilter::new().apply(&entries);
215 assert_eq!(results.len(), 3);
216 }
217
218 #[test]
219 fn filter_before_timestamp() {
220 let entries = make_chain();
221 let last_ts = entries[2].timestamp();
222 let results = QueryFilter::new().before(last_ts).apply(&entries);
223 assert!(results.iter().all(|e| e.timestamp() < last_ts));
224 }
225
226 #[test]
227 fn filter_min_severity() {
228 let entries = make_chain();
229 let results = QueryFilter::new()
231 .min_severity(EventSeverity::Security)
232 .apply(&entries);
233 assert_eq!(results.len(), 1);
234 assert_eq!(results[0].source(), "aegis");
235
236 let results = QueryFilter::new()
238 .min_severity(EventSeverity::Info)
239 .apply(&entries);
240 assert_eq!(results.len(), 3);
241 }
242
243 #[test]
244 fn filter_no_matches() {
245 let entries = make_chain();
246 let results = QueryFilter::new().source("nonexistent").apply(&entries);
247 assert!(results.is_empty());
248 }
249}