Skip to content

Commit e600ca2

Browse files
authored
Merge pull request #42 from timescale/fix/pysigma-parity
feat: implement pySigma parity gaps
2 parents 212edd1 + d204d81 commit e600ca2

11 files changed

Lines changed: 393 additions & 126 deletions

File tree

crates/rsigma-cli/tests/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ filter:
110110
- 00000000-0000-0000-0000-000000000020
111111
selection:
112112
TargetFilename|endswith: "\\trusted.exe"
113-
condition: selection
113+
condition: not selection
114114
"#;
115115

116116
const PIPELINE_YAML: &str = r#"

crates/rsigma-convert/src/backends/postgres.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,9 @@ impl Backend for PostgresBackend {
713713
let having_clause = self.build_having_clause(&rule.condition)?;
714714

715715
let field_from_condition = match &rule.condition {
716-
CorrelationCondition::Threshold { field, .. } => field.clone(),
716+
CorrelationCondition::Threshold { field, .. } => {
717+
field.as_ref().and_then(|f| f.first().cloned())
718+
}
717719
_ => None,
718720
};
719721
let value_field = field_from_condition.as_deref().or_else(|| {
@@ -801,7 +803,12 @@ impl Backend for PostgresBackend {
801803
let percentile = if rule.correlation_type == CorrelationType::ValueMedian {
802804
0.5
803805
} else {
804-
0.95
806+
match &rule.condition {
807+
CorrelationCondition::Threshold { percentile, .. } => {
808+
percentile.map(|p| p as f64 / 100.0).unwrap_or(0.95)
809+
}
810+
_ => 0.95,
811+
}
805812
};
806813
let agg = format!("PERCENTILE_CONT({percentile}) WITHIN GROUP (ORDER BY {field})");
807814
format!(

crates/rsigma-eval/src/correlation.rs

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,14 @@ impl GroupByField {
108108
/// Compiled threshold condition with one or more predicates (supports ranges).
109109
#[derive(Debug, Clone)]
110110
pub struct CompiledCondition {
111-
/// Optional field name for value_count, value_sum, value_avg, value_percentile.
112-
pub field: Option<String>,
111+
/// Optional field name(s) for value_count, value_sum, value_avg, value_percentile.
112+
/// When multiple fields are present, value_count counts distinct tuples.
113+
pub field: Option<Vec<String>>,
113114
/// One or more predicates to satisfy (all must be true for the condition to match).
114115
pub predicates: Vec<(ConditionOperator, f64)>,
116+
/// Percentile rank (0-100) for `value_percentile` type.
117+
/// `None` means use the default (50).
118+
pub percentile: Option<u64>,
115119
}
116120

117121
impl CompiledCondition {
@@ -666,12 +670,7 @@ impl WindowState {
666670
return None;
667671
}
668672
values.sort_by(|a, b| a.partial_cmp(b).expect("NaN filtered"));
669-
// Extract the percentile rank from the condition's first predicate
670-
let percentile_rank = condition
671-
.predicates
672-
.first()
673-
.map(|(_, threshold)| *threshold)
674-
.unwrap_or(50.0);
673+
let percentile_rank = condition.percentile.map(|p| p as f64).unwrap_or(50.0);
675674
let pval = percentile_linear_interp(&values, percentile_rank);
676675
return Some(pval);
677676
}
@@ -894,13 +893,18 @@ fn compile_condition(
894893
corr_type: CorrelationType,
895894
) -> Result<(CompiledCondition, Option<ConditionExpr>)> {
896895
match cond {
897-
CorrelationCondition::Threshold { predicates, field } => Ok((
896+
CorrelationCondition::Threshold {
897+
predicates,
898+
field,
899+
percentile,
900+
} => Ok((
898901
CompiledCondition {
899902
field: field.clone(),
900903
predicates: predicates
901904
.iter()
902905
.map(|(op, count)| (*op, *count as f64))
903906
.collect(),
907+
percentile: *percentile,
904908
},
905909
None,
906910
)),
@@ -913,6 +917,7 @@ fn compile_condition(
913917
CompiledCondition {
914918
field: None,
915919
predicates: vec![(ConditionOperator::Gte, 1.0)],
920+
percentile: None,
916921
},
917922
Some(expr.clone()),
918923
))
@@ -979,6 +984,7 @@ mod tests {
979984
let cond = CompiledCondition {
980985
field: None,
981986
predicates: vec![(ConditionOperator::Gte, 100.0)],
987+
percentile: None,
982988
};
983989
assert!(!cond.check(99.0));
984990
assert!(cond.check(100.0));
@@ -993,6 +999,7 @@ mod tests {
993999
(ConditionOperator::Gt, 100.0),
9941000
(ConditionOperator::Lte, 200.0),
9951001
],
1002+
percentile: None,
9961003
};
9971004
assert!(!cond.check(100.0));
9981005
assert!(cond.check(101.0));
@@ -1009,6 +1016,7 @@ mod tests {
10091016
let cond = CompiledCondition {
10101017
field: None,
10111018
predicates: vec![(ConditionOperator::Gte, 5.0)],
1019+
percentile: None,
10121020
};
10131021
assert_eq!(
10141022
state.check_condition(&cond, CorrelationType::EventCount, &[], None),
@@ -1027,6 +1035,7 @@ mod tests {
10271035
let cond = CompiledCondition {
10281036
field: None,
10291037
predicates: vec![(ConditionOperator::Gte, 5.0)],
1038+
percentile: None,
10301039
};
10311040
assert_eq!(
10321041
state.check_condition(&cond, CorrelationType::EventCount, &[], None),
@@ -1043,8 +1052,9 @@ mod tests {
10431052
state.push_value_count(1003, "user3".to_string());
10441053

10451054
let cond = CompiledCondition {
1046-
field: Some("User".to_string()),
1055+
field: Some(vec!["User".to_string()]),
10471056
predicates: vec![(ConditionOperator::Gte, 3.0)],
1057+
percentile: None,
10481058
};
10491059
assert_eq!(
10501060
state.check_condition(&cond, CorrelationType::ValueCount, &[], None),
@@ -1061,6 +1071,7 @@ mod tests {
10611071
let cond = CompiledCondition {
10621072
field: None,
10631073
predicates: vec![(ConditionOperator::Gte, 2.0)],
1074+
percentile: None,
10641075
};
10651076
assert!(
10661077
state
@@ -1092,6 +1103,7 @@ mod tests {
10921103
let cond = CompiledCondition {
10931104
field: None,
10941105
predicates: vec![(ConditionOperator::Gte, 3.0)],
1106+
percentile: None,
10951107
};
10961108
assert!(
10971109
state
@@ -1111,6 +1123,7 @@ mod tests {
11111123
let cond = CompiledCondition {
11121124
field: None,
11131125
predicates: vec![(ConditionOperator::Gte, 2.0)],
1126+
percentile: None,
11141127
};
11151128
assert!(
11161129
state
@@ -1126,8 +1139,9 @@ mod tests {
11261139
state.push_numeric(1001, 600.0);
11271140

11281141
let cond = CompiledCondition {
1129-
field: Some("bytes_sent".to_string()),
1142+
field: Some(vec!["bytes_sent".to_string()]),
11301143
predicates: vec![(ConditionOperator::Gt, 1000.0)],
1144+
percentile: None,
11311145
};
11321146
assert_eq!(
11331147
state.check_condition(&cond, CorrelationType::ValueSum, &[], None),
@@ -1143,8 +1157,9 @@ mod tests {
11431157
state.push_numeric(1002, 300.0);
11441158

11451159
let cond = CompiledCondition {
1146-
field: Some("bytes".to_string()),
1160+
field: Some(vec!["bytes".to_string()]),
11471161
predicates: vec![(ConditionOperator::Gte, 200.0)],
1162+
percentile: None,
11481163
};
11491164
assert_eq!(
11501165
state.check_condition(&cond, CorrelationType::ValueAvg, &[], None),
@@ -1160,8 +1175,9 @@ mod tests {
11601175
state.push_numeric(1002, 30.0);
11611176

11621177
let cond = CompiledCondition {
1163-
field: Some("latency".to_string()),
1178+
field: Some(vec!["latency".to_string()]),
11641179
predicates: vec![(ConditionOperator::Gte, 20.0)],
1180+
percentile: None,
11651181
};
11661182
assert_eq!(
11671183
state.check_condition(&cond, CorrelationType::ValueMedian, &[], None),
@@ -1289,6 +1305,7 @@ level: high
12891305
let cond = CompiledCondition {
12901306
field: None,
12911307
predicates: vec![(ConditionOperator::Gte, 1.0)],
1308+
percentile: None,
12921309
};
12931310
let expr = ConditionExpr::And(vec![
12941311
ConditionExpr::Identifier("rule_a".to_string()),
@@ -1363,9 +1380,9 @@ level: high
13631380
}
13641381

13651382
let cond = CompiledCondition {
1366-
field: Some("latency".to_string()),
1367-
// The condition threshold is used as the percentile rank
1383+
field: Some(vec!["latency".to_string()]),
13681384
predicates: vec![(ConditionOperator::Lte, 50.0)],
1385+
percentile: None,
13691386
};
13701387
// 50th percentile of 1..100 should be ~50.5
13711388
let result = state.check_condition(&cond, CorrelationType::ValuePercentile, &[], None);
@@ -1403,8 +1420,9 @@ level: high
14031420
fn test_value_percentile_empty_window() {
14041421
let state = WindowState::new_for(CorrelationType::ValuePercentile);
14051422
let cond = CompiledCondition {
1406-
field: Some("latency".to_string()),
1423+
field: Some(vec!["latency".to_string()]),
14071424
predicates: vec![(ConditionOperator::Lte, 50.0)],
1425+
percentile: None,
14081426
};
14091427
// Empty window should return None
14101428
assert!(
@@ -1477,6 +1495,7 @@ level: high
14771495
let cond = CompiledCondition {
14781496
field: None,
14791497
predicates: vec![(ConditionOperator::Gte, 2.0)],
1498+
percentile: None,
14801499
};
14811500
// Without extended expr: 2 of 3 rules fired, meets gte 2
14821501
assert_eq!(
@@ -1488,6 +1507,7 @@ level: high
14881507
let cond3 = CompiledCondition {
14891508
field: None,
14901509
predicates: vec![(ConditionOperator::Gte, 3.0)],
1510+
percentile: None,
14911511
};
14921512
assert!(
14931513
state
@@ -1721,18 +1741,23 @@ level: high
17211741
author: None,
17221742
date: None,
17231743
modified: None,
1744+
related: vec![],
17241745
references: vec![],
17251746
taxonomy: None,
1747+
license: None,
17261748
tags: vec![],
1749+
fields: vec![],
17271750
falsepositives: vec![],
17281751
level: Some(Level::High),
1752+
scope: vec![],
17291753
correlation_type: CorrelationType::EventCount,
17301754
rules: vec!["rule-1".to_string()],
17311755
group_by: vec!["User".to_string()],
17321756
timespan: Timespan::parse("60s").unwrap(),
17331757
condition: CorrelationCondition::Threshold {
17341758
predicates: vec![(ConditionOperator::Gte, 5)],
17351759
field: None,
1760+
percentile: None,
17361761
},
17371762
aliases: vec![],
17381763
generate: false,

crates/rsigma-eval/src/correlation_engine.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,8 @@ impl CorrelationEngine {
862862
state.push_event_count(ts);
863863
}
864864
CorrelationType::ValueCount => {
865-
if let Some(ref field_name) = corr.condition.field
865+
if let Some(ref fields) = corr.condition.field
866+
&& let Some(field_name) = fields.first()
866867
&& let Some(val) = event.get_field(field_name)
867868
&& let Some(s) = value_to_string_for_count(&val)
868869
{
@@ -876,7 +877,8 @@ impl CorrelationEngine {
876877
| CorrelationType::ValueAvg
877878
| CorrelationType::ValuePercentile
878879
| CorrelationType::ValueMedian => {
879-
if let Some(ref field_name) = corr.condition.field
880+
if let Some(ref fields) = corr.condition.field
881+
&& let Some(field_name) = fields.first()
880882
&& let Some(val) = event.get_field(field_name)
881883
&& let Some(n) = value_to_f64_ev(&val)
882884
{
@@ -2664,7 +2666,7 @@ filter:
26642666
- failed-auth
26652667
selection:
26662668
User|startswith: 'svc_'
2667-
condition: selection
2669+
condition: not selection
26682670
---
26692671
title: Brute Force
26702672
correlation:

0 commit comments

Comments
 (0)