1use std::collections::{HashMap, HashSet, VecDeque};
2
3use rsigma_parser::{ConditionExpr, CorrelationType, WindowMode};
4use serde::Serialize;
5
6use super::CompiledCondition;
7
8#[derive(Debug, Clone, Serialize, serde::Deserialize)]
16pub enum WindowState {
17 EventCount { timestamps: VecDeque<i64> },
19 ValueCount { entries: VecDeque<(i64, String)> },
21 Temporal {
23 rule_hits: HashMap<String, VecDeque<i64>>,
24 },
25 NumericAgg { entries: VecDeque<(i64, f64)> },
28}
29
30impl WindowState {
31 pub fn new_for(corr_type: CorrelationType) -> Self {
33 match corr_type {
34 CorrelationType::EventCount => WindowState::EventCount {
35 timestamps: VecDeque::new(),
36 },
37 CorrelationType::ValueCount => WindowState::ValueCount {
38 entries: VecDeque::new(),
39 },
40 CorrelationType::Temporal | CorrelationType::TemporalOrdered => WindowState::Temporal {
41 rule_hits: HashMap::new(),
42 },
43 CorrelationType::ValueSum
44 | CorrelationType::ValueAvg
45 | CorrelationType::ValuePercentile
46 | CorrelationType::ValueMedian => WindowState::NumericAgg {
47 entries: VecDeque::new(),
48 },
49 }
50 }
51
52 pub fn evict(&mut self, cutoff: i64) {
54 match self {
55 WindowState::EventCount { timestamps } => {
56 while timestamps.front().is_some_and(|&t| t < cutoff) {
57 timestamps.pop_front();
58 }
59 }
60 WindowState::ValueCount { entries } => {
61 while entries.front().is_some_and(|(t, _)| *t < cutoff) {
62 entries.pop_front();
63 }
64 }
65 WindowState::Temporal { rule_hits } => {
66 for timestamps in rule_hits.values_mut() {
67 while timestamps.front().is_some_and(|&t| t < cutoff) {
68 timestamps.pop_front();
69 }
70 }
71 rule_hits.retain(|_, ts| !ts.is_empty());
73 }
74 WindowState::NumericAgg { entries } => {
75 while entries.front().is_some_and(|(t, _)| *t < cutoff) {
76 entries.pop_front();
77 }
78 }
79 }
80 }
81
82 pub fn is_empty(&self) -> bool {
84 match self {
85 WindowState::EventCount { timestamps } => timestamps.is_empty(),
86 WindowState::ValueCount { entries } => entries.is_empty(),
87 WindowState::Temporal { rule_hits } => rule_hits.is_empty(),
88 WindowState::NumericAgg { entries } => entries.is_empty(),
89 }
90 }
91
92 pub fn latest_timestamp(&self) -> Option<i64> {
94 match self {
95 WindowState::EventCount { timestamps } => timestamps.back().copied(),
96 WindowState::ValueCount { entries } => entries.back().map(|(t, _)| *t),
97 WindowState::Temporal { rule_hits } => {
98 rule_hits.values().filter_map(|ts| ts.back().copied()).max()
99 }
100 WindowState::NumericAgg { entries } => entries.back().map(|(t, _)| *t),
101 }
102 }
103
104 pub fn earliest_timestamp(&self) -> Option<i64> {
110 match self {
111 WindowState::EventCount { timestamps } => timestamps.front().copied(),
112 WindowState::ValueCount { entries } => entries.front().map(|(t, _)| *t),
113 WindowState::Temporal { rule_hits } => rule_hits
114 .values()
115 .filter_map(|ts| ts.front().copied())
116 .min(),
117 WindowState::NumericAgg { entries } => entries.front().map(|(t, _)| *t),
118 }
119 }
120
121 pub fn truncate_oldest(&mut self, cap: usize, preserve_front: bool) {
133 let cap = cap.max(1);
134
135 fn truncate_deque<T>(deque: &mut VecDeque<T>, cap: usize, preserve_front: bool) {
136 if deque.len() <= cap {
137 return;
138 }
139 if preserve_front {
140 let anchor = deque.pop_front().expect("len > cap >= 1");
141 let excess = deque.len() - (cap - 1);
143 deque.drain(..excess);
144 deque.push_front(anchor);
145 } else {
146 let excess = deque.len() - cap;
147 deque.drain(..excess);
148 }
149 }
150
151 match self {
152 WindowState::EventCount { timestamps } => {
153 truncate_deque(timestamps, cap, preserve_front);
154 }
155 WindowState::ValueCount { entries } => {
156 truncate_deque(entries, cap, preserve_front);
157 }
158 WindowState::Temporal { rule_hits } => {
159 for timestamps in rule_hits.values_mut() {
160 truncate_deque(timestamps, cap, preserve_front);
161 }
162 }
163 WindowState::NumericAgg { entries } => {
164 truncate_deque(entries, cap, preserve_front);
165 }
166 }
167 }
168
169 pub fn clear(&mut self) {
171 match self {
172 WindowState::EventCount { timestamps } => timestamps.clear(),
173 WindowState::ValueCount { entries } => entries.clear(),
174 WindowState::Temporal { rule_hits } => rule_hits.clear(),
175 WindowState::NumericAgg { entries } => entries.clear(),
176 }
177 }
178
179 pub fn push_event_count(&mut self, ts: i64) {
181 if let WindowState::EventCount { timestamps } = self {
182 timestamps.push_back(ts);
183 }
184 }
185
186 pub fn push_value_count(&mut self, ts: i64, value: String) {
188 if let WindowState::ValueCount { entries } = self {
189 entries.push_back((ts, value));
190 }
191 }
192
193 pub fn push_temporal(&mut self, ts: i64, rule_ref: &str) {
195 if let WindowState::Temporal { rule_hits } = self {
196 rule_hits
197 .entry(rule_ref.to_string())
198 .or_default()
199 .push_back(ts);
200 }
201 }
202
203 pub fn push_numeric(&mut self, ts: i64, value: f64) {
205 if let WindowState::NumericAgg { entries } = self {
206 entries.push_back((ts, value));
207 }
208 }
209
210 pub fn check_condition(
218 &self,
219 condition: &CompiledCondition,
220 corr_type: CorrelationType,
221 rule_refs: &[String],
222 extended_expr: Option<&ConditionExpr>,
223 ) -> Option<f64> {
224 let value = match (self, corr_type) {
225 (WindowState::EventCount { timestamps }, CorrelationType::EventCount) => {
226 timestamps.len() as f64
227 }
228 (WindowState::ValueCount { entries }, CorrelationType::ValueCount) => {
229 let distinct: HashSet<&String> = entries.iter().map(|(_, v)| v).collect();
231 distinct.len() as f64
232 }
233 (WindowState::Temporal { rule_hits }, CorrelationType::Temporal) => {
234 if let Some(expr) = extended_expr {
236 if eval_temporal_expr(expr, rule_hits) {
237 return Some(count_fired_rules(rule_refs, rule_hits) as f64);
238 } else {
239 return None;
240 }
241 }
242 count_fired_rules(rule_refs, rule_hits) as f64
243 }
244 (WindowState::Temporal { rule_hits }, CorrelationType::TemporalOrdered) => {
245 if let Some(expr) = extended_expr
247 && !eval_temporal_expr(expr, rule_hits)
248 {
249 return None;
250 }
251 if check_temporal_ordered(rule_refs, rule_hits) {
253 count_distinct_rules(rule_refs) as f64
254 } else {
255 0.0
256 }
257 }
258 (WindowState::NumericAgg { entries }, CorrelationType::ValueSum) => {
259 entries.iter().map(|(_, v)| v).sum()
260 }
261 (WindowState::NumericAgg { entries }, CorrelationType::ValueAvg) => {
262 if entries.is_empty() {
263 0.0
264 } else {
265 let sum: f64 = entries.iter().map(|(_, v)| v).sum();
266 sum / entries.len() as f64
267 }
268 }
269 (WindowState::NumericAgg { entries }, CorrelationType::ValuePercentile) => {
270 if entries.is_empty() {
274 return None;
275 }
276 let mut values: Vec<f64> = entries
277 .iter()
278 .map(|(_, v)| *v)
279 .filter(|v| v.is_finite())
280 .collect();
281 if values.is_empty() {
282 return None;
283 }
284 values.sort_by(|a, b| a.total_cmp(b));
285 let percentile_rank = condition.percentile.map(|p| p as f64).unwrap_or(50.0);
286 let pval = percentile_linear_interp(&values, percentile_rank);
287 return Some(pval);
288 }
289 (WindowState::NumericAgg { entries }, CorrelationType::ValueMedian) => {
290 if entries.is_empty() {
294 return None;
295 }
296 let mut values: Vec<f64> = entries
297 .iter()
298 .map(|(_, v)| *v)
299 .filter(|v| v.is_finite())
300 .collect();
301 if values.is_empty() {
302 return None;
303 }
304 values.sort_by(|a, b| a.total_cmp(b));
305 let mid = values.len() / 2;
306 if values.len().is_multiple_of(2) && values.len() >= 2 {
307 (values[mid - 1] + values[mid]) / 2.0
308 } else {
309 values[mid]
310 }
311 }
312 _ => return None, };
314
315 if condition.check(value) {
316 Some(value)
317 } else {
318 None
319 }
320 }
321}
322
323impl WindowState {
324 pub fn entry_count(&self) -> usize {
327 match self {
328 WindowState::EventCount { timestamps } => timestamps.len(),
329 WindowState::ValueCount { entries } => entries.len(),
330 WindowState::Temporal { rule_hits } => rule_hits.values().map(VecDeque::len).sum(),
331 WindowState::NumericAgg { entries } => entries.len(),
332 }
333 }
334
335 pub fn current_value(
344 &self,
345 corr_type: CorrelationType,
346 rule_refs: &[String],
347 percentile: Option<u64>,
348 ) -> Option<f64> {
349 match (self, corr_type) {
350 (WindowState::EventCount { timestamps }, CorrelationType::EventCount) => {
351 Some(timestamps.len() as f64)
352 }
353 (WindowState::ValueCount { entries }, CorrelationType::ValueCount) => {
354 let distinct: HashSet<&String> = entries.iter().map(|(_, v)| v).collect();
355 Some(distinct.len() as f64)
356 }
357 (
358 WindowState::Temporal { rule_hits },
359 CorrelationType::Temporal | CorrelationType::TemporalOrdered,
360 ) => Some(count_fired_rules(rule_refs, rule_hits) as f64),
361 (WindowState::NumericAgg { entries }, CorrelationType::ValueSum) => {
362 Some(entries.iter().map(|(_, v)| v).sum())
363 }
364 (WindowState::NumericAgg { entries }, CorrelationType::ValueAvg) => {
365 if entries.is_empty() {
366 Some(0.0)
367 } else {
368 let sum: f64 = entries.iter().map(|(_, v)| v).sum();
369 Some(sum / entries.len() as f64)
370 }
371 }
372 (WindowState::NumericAgg { entries }, CorrelationType::ValuePercentile) => {
373 let mut values: Vec<f64> = entries
374 .iter()
375 .map(|(_, v)| *v)
376 .filter(|v| v.is_finite())
377 .collect();
378 if values.is_empty() {
379 return None;
380 }
381 values.sort_by(|a, b| a.total_cmp(b));
382 let rank = percentile.map(|p| p as f64).unwrap_or(50.0);
383 Some(percentile_linear_interp(&values, rank))
384 }
385 (WindowState::NumericAgg { entries }, CorrelationType::ValueMedian) => {
386 let mut values: Vec<f64> = entries
387 .iter()
388 .map(|(_, v)| *v)
389 .filter(|v| v.is_finite())
390 .collect();
391 if values.is_empty() {
392 return None;
393 }
394 values.sort_by(|a, b| a.total_cmp(b));
395 let mid = values.len() / 2;
396 let v = if values.len().is_multiple_of(2) && values.len() >= 2 {
397 (values[mid - 1] + values[mid]) / 2.0
398 } else {
399 values[mid]
400 };
401 Some(v)
402 }
403 _ => None,
404 }
405 }
406}
407
408fn bucket_start(ts: i64, timespan: i64) -> i64 {
413 ts - ts.rem_euclid(timespan)
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418pub enum WindowDecision {
419 Extend,
421 Reset,
425 Discard,
428}
429
430pub fn apply_window_open(
445 state: &mut WindowState,
446 ts: i64,
447 timespan_secs: u64,
448 window: WindowMode,
449 gap_secs: Option<u64>,
450) -> WindowDecision {
451 let timespan = timespan_secs as i64;
452 match window {
453 WindowMode::Sliding => {
454 state.evict(ts - timespan);
455 WindowDecision::Extend
456 }
457 WindowMode::Tumbling => {
458 if timespan <= 0 {
459 return WindowDecision::Extend;
460 }
461 match state.latest_timestamp() {
462 Some(last) if bucket_start(ts, timespan) > bucket_start(last, timespan) => {
463 state.clear();
464 WindowDecision::Reset
465 }
466 Some(last) if bucket_start(ts, timespan) < bucket_start(last, timespan) => {
467 WindowDecision::Discard
468 }
469 _ => WindowDecision::Extend,
470 }
471 }
472 WindowMode::Session => {
473 let gap = gap_secs.map(|g| g as i64).unwrap_or(timespan);
476 let reset = match (state.earliest_timestamp(), state.latest_timestamp()) {
477 (Some(start), Some(last)) => {
478 (ts - last) > gap || (timespan > 0 && (ts - start) > timespan)
479 }
480 _ => false,
481 };
482 if reset {
483 state.clear();
484 WindowDecision::Reset
485 } else {
486 WindowDecision::Extend
487 }
488 }
489 }
490}
491
492fn count_distinct_rules(rule_refs: &[String]) -> usize {
495 rule_refs.iter().collect::<HashSet<_>>().len()
496}
497
498fn count_fired_rules(rule_refs: &[String], rule_hits: &HashMap<String, VecDeque<i64>>) -> usize {
500 rule_refs
501 .iter()
502 .filter(|r| rule_hits.get(r.as_str()).is_some_and(|ts| !ts.is_empty()))
503 .collect::<HashSet<_>>()
504 .len()
505}
506
507fn check_temporal_ordered(
513 rule_refs: &[String],
514 rule_hits: &HashMap<String, VecDeque<i64>>,
515) -> bool {
516 if rule_refs.is_empty() {
517 return true;
518 }
519
520 for r in rule_refs {
522 if rule_hits.get(r.as_str()).is_none_or(|ts| ts.is_empty()) {
523 return false;
524 }
525 }
526
527 fn find_ordered(
530 rule_refs: &[String],
531 rule_hits: &HashMap<String, VecDeque<i64>>,
532 idx: usize,
533 min_ts: i64,
534 ) -> bool {
535 if idx >= rule_refs.len() {
536 return true;
537 }
538 let Some(timestamps) = rule_hits.get(&rule_refs[idx]) else {
539 return false;
540 };
541 for &ts in timestamps {
542 if ts >= min_ts && find_ordered(rule_refs, rule_hits, idx + 1, ts) {
543 return true;
544 }
545 }
546 false
547 }
548
549 find_ordered(rule_refs, rule_hits, 0, i64::MIN)
550}
551
552pub(super) fn eval_temporal_expr(
558 expr: &ConditionExpr,
559 rule_hits: &HashMap<String, VecDeque<i64>>,
560) -> bool {
561 match expr {
562 ConditionExpr::Identifier(name) => rule_hits
563 .get(name.as_str())
564 .is_some_and(|ts| !ts.is_empty()),
565 ConditionExpr::And(children) => children.iter().all(|c| eval_temporal_expr(c, rule_hits)),
566 ConditionExpr::Or(children) => children.iter().any(|c| eval_temporal_expr(c, rule_hits)),
567 ConditionExpr::Not(child) => !eval_temporal_expr(child, rule_hits),
568 ConditionExpr::Selector { .. } => {
569 false
571 }
572 }
573}
574
575pub(super) fn percentile_linear_interp(values: &[f64], percentile: f64) -> f64 {
581 if values.is_empty() {
582 return 0.0;
583 }
584 let n = values.len();
585 if n == 1 {
586 return values[0];
587 }
588
589 let p = percentile.clamp(0.0, 100.0) / 100.0;
591
592 let rank = p * (n - 1) as f64;
595 let lower = rank.floor() as usize;
596 let upper = rank.ceil() as usize;
597 let fraction = rank - lower as f64;
598
599 if lower == upper || upper >= n {
600 values[lower.min(n - 1)]
601 } else {
602 values[lower] + fraction * (values[upper] - values[lower])
603 }
604}