@@ -108,10 +108,14 @@ impl GroupByField {
108108/// Compiled threshold condition with one or more predicates (supports ranges).
109109#[ derive( Debug , Clone ) ]
110110pub 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
117121impl 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 ,
0 commit comments