Skip to content

Commit ee98b42

Browse files
authored
Add composite alerts types for multiple columns (#472)
1 parent b178a05 commit ee98b42

File tree

6 files changed

+546
-36
lines changed

6 files changed

+546
-36
lines changed

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ ulid = { version = "1.0", features = ["serde"] }
8787
uptime_lib = "0.2.2"
8888
xxhash-rust = { version = "0.8", features = ["xxh3"] }
8989
xz2 = { version = "*", features = ["static"] }
90+
nom = "7.1.3"
9091

9192

9293
[build-dependencies]

server/src/alerts/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use regex::Regex;
2626
use serde::{Deserialize, Serialize};
2727
use std::fmt;
2828

29+
pub mod parser;
2930
pub mod rule;
3031
pub mod target;
3132

server/src/alerts/parser.rs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/*
2+
* Parseable Server (C) 2022 - 2023 Parseable, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*
17+
*/
18+
19+
use std::str::FromStr;
20+
21+
use nom::{
22+
branch::alt,
23+
bytes::complete::{tag, take_until, take_while1},
24+
character::complete::{char, multispace0, multispace1},
25+
combinator::map,
26+
sequence::{delimited, separated_pair},
27+
IResult,
28+
};
29+
30+
use super::rule::{
31+
base::{
32+
ops::{NumericOperator, StringOperator},
33+
NumericRule, StringRule,
34+
},
35+
CompositeRule,
36+
};
37+
38+
fn parse_numeric_op(input: &str) -> IResult<&str, NumericOperator> {
39+
alt((
40+
map(tag("<="), |_| NumericOperator::LessThanEquals),
41+
map(tag(">="), |_| NumericOperator::GreaterThanEquals),
42+
map(tag("!="), |_| NumericOperator::NotEqualTo),
43+
map(tag("<"), |_| NumericOperator::LessThan),
44+
map(tag(">"), |_| NumericOperator::GreaterThan),
45+
map(tag("="), |_| NumericOperator::EqualTo),
46+
))(input)
47+
}
48+
49+
fn parse_string_op(input: &str) -> IResult<&str, StringOperator> {
50+
alt((
51+
map(tag("!="), |_| StringOperator::NotExact),
52+
map(tag("=%"), |_| StringOperator::Contains),
53+
map(tag("!%"), |_| StringOperator::NotContains),
54+
map(tag("="), |_| StringOperator::Exact),
55+
map(tag("~"), |_| StringOperator::Regex),
56+
))(input)
57+
}
58+
59+
fn parse_numeric_rule(input: &str) -> IResult<&str, CompositeRule> {
60+
let (remaining, key) = map(parse_identifier, |s: &str| s.to_string())(input)?;
61+
let (remaining, op) = delimited(multispace0, parse_numeric_op, multispace0)(remaining)?;
62+
let (remaining, value) = map(take_while1(|c: char| c.is_ascii_digit()), |x| {
63+
str::parse(x).unwrap()
64+
})(remaining)?;
65+
66+
Ok((
67+
remaining,
68+
CompositeRule::Numeric(NumericRule {
69+
column: key,
70+
operator: op,
71+
value,
72+
}),
73+
))
74+
}
75+
76+
fn parse_string_rule(input: &str) -> IResult<&str, CompositeRule> {
77+
let (remaining, key) = map(parse_identifier, |s: &str| s.to_string())(input)?;
78+
let (remaining, op) = delimited(multispace0, parse_string_op, multispace0)(remaining)?;
79+
let (remaining, value) = map(
80+
delimited(char('"'), take_until("\""), char('"')),
81+
|x: &str| x.to_string(),
82+
)(remaining)?;
83+
84+
Ok((
85+
remaining,
86+
CompositeRule::String(StringRule {
87+
column: key,
88+
operator: op,
89+
value,
90+
ignore_case: None,
91+
}),
92+
))
93+
}
94+
95+
fn parse_identifier(input: &str) -> IResult<&str, &str> {
96+
take_while1(|c: char| c.is_alphanumeric() || c == '-' || c == '_')(input)
97+
}
98+
99+
fn parse_unary_expr(input: &str) -> IResult<&str, CompositeRule> {
100+
map(delimited(tag("!("), parse_expression, char(')')), |x| {
101+
CompositeRule::Not(Box::new(x))
102+
})(input)
103+
}
104+
105+
fn parse_bracket_expr(input: &str) -> IResult<&str, CompositeRule> {
106+
delimited(
107+
char('('),
108+
delimited(multispace0, parse_expression, multispace0),
109+
char(')'),
110+
)(input)
111+
}
112+
113+
fn parse_and(input: &str) -> IResult<&str, CompositeRule> {
114+
let (remaining, (lhs, rhs)) = separated_pair(
115+
parse_atom,
116+
delimited(multispace1, tag("and"), multispace1),
117+
parse_term,
118+
)(input)?;
119+
120+
Ok((remaining, CompositeRule::And(vec![lhs, rhs])))
121+
}
122+
123+
fn parse_or(input: &str) -> IResult<&str, CompositeRule> {
124+
let (remaining, (lhs, rhs)) = separated_pair(
125+
parse_term,
126+
delimited(multispace1, tag("or"), multispace1),
127+
parse_expression,
128+
)(input)?;
129+
130+
Ok((remaining, CompositeRule::Or(vec![lhs, rhs])))
131+
}
132+
133+
fn parse_expression(input: &str) -> IResult<&str, CompositeRule> {
134+
alt((parse_or, parse_term))(input)
135+
}
136+
fn parse_term(input: &str) -> IResult<&str, CompositeRule> {
137+
alt((parse_and, parse_atom))(input)
138+
}
139+
fn parse_atom(input: &str) -> IResult<&str, CompositeRule> {
140+
alt((
141+
alt((parse_numeric_rule, parse_string_rule)),
142+
parse_unary_expr,
143+
parse_bracket_expr,
144+
))(input)
145+
}
146+
147+
impl FromStr for CompositeRule {
148+
type Err = Box<dyn std::error::Error>;
149+
150+
fn from_str(s: &str) -> Result<Self, Self::Err> {
151+
parse_expression(s)
152+
.map(|(_, x)| x)
153+
.map_err(|x| x.to_string().into())
154+
}
155+
}
156+
157+
#[cfg(test)]
158+
mod tests {
159+
use std::str::FromStr;
160+
161+
use crate::alerts::rule::{
162+
base::{
163+
ops::{NumericOperator, StringOperator},
164+
NumericRule, StringRule,
165+
},
166+
CompositeRule,
167+
};
168+
169+
#[test]
170+
fn test_and_or_not() {
171+
let input = r#"key=500 and key="value" or !(key=300)"#;
172+
let rule = CompositeRule::from_str(input).unwrap();
173+
174+
let numeric1 = NumericRule {
175+
column: "key".to_string(),
176+
operator: NumericOperator::EqualTo,
177+
value: serde_json::Number::from(500),
178+
};
179+
180+
let string1 = StringRule {
181+
column: "key".to_string(),
182+
operator: StringOperator::Exact,
183+
value: "value".to_string(),
184+
ignore_case: None,
185+
};
186+
187+
let numeric3 = NumericRule {
188+
column: "key".to_string(),
189+
operator: NumericOperator::EqualTo,
190+
value: serde_json::Number::from(300),
191+
};
192+
193+
assert_eq!(
194+
rule,
195+
CompositeRule::Or(vec![
196+
CompositeRule::And(vec![
197+
CompositeRule::Numeric(numeric1),
198+
CompositeRule::String(string1)
199+
]),
200+
CompositeRule::Not(Box::new(CompositeRule::Numeric(numeric3)))
201+
])
202+
)
203+
}
204+
205+
#[test]
206+
fn test_complex() {
207+
let input = r#"(verb =% "list" or verb =% "get") and (resource = "secret" and username !% "admin")"#;
208+
let rule = CompositeRule::from_str(input).unwrap();
209+
210+
let verb_like_list = StringRule {
211+
column: "verb".to_string(),
212+
operator: StringOperator::Contains,
213+
value: "list".to_string(),
214+
ignore_case: None,
215+
};
216+
217+
let verb_like_get = StringRule {
218+
column: "verb".to_string(),
219+
operator: StringOperator::Contains,
220+
value: "get".to_string(),
221+
ignore_case: None,
222+
};
223+
224+
let resource_exact_secret = StringRule {
225+
column: "resource".to_string(),
226+
operator: StringOperator::Exact,
227+
value: "secret".to_string(),
228+
ignore_case: None,
229+
};
230+
231+
let username_notcontains_admin = StringRule {
232+
column: "username".to_string(),
233+
operator: StringOperator::NotContains,
234+
value: "admin".to_string(),
235+
ignore_case: None,
236+
};
237+
238+
assert_eq!(
239+
rule,
240+
CompositeRule::And(vec![
241+
CompositeRule::Or(vec![
242+
CompositeRule::String(verb_like_list),
243+
CompositeRule::String(verb_like_get)
244+
]),
245+
CompositeRule::And(vec![
246+
CompositeRule::String(resource_exact_secret),
247+
CompositeRule::String(username_notcontains_admin)
248+
]),
249+
])
250+
)
251+
}
252+
}

0 commit comments

Comments
 (0)