extern crate nom;
use std::io::BufRead;
use nom::{
bytes::complete::tag,
character::complete::multispace0,
error::{ErrorKind, ParseError},
multi::{many0, many0_count},
sequence::{preceded, tuple},
IResult,
};
static WHITESPACE: &str = " \t\r\n";
#[derive(Debug, PartialEq)]
enum InternalError<I> {
NoListEnd,
Nom(I, ErrorKind),
}
impl<I> ParseError<I> for InternalError<I> {
fn from_error_kind(input: I, kind: ErrorKind) -> Self {
InternalError::Nom(input, kind)
}
fn append(_input: I, _kind: ErrorKind, other: Self) -> Self {
other
}
}
#[derive(Debug, PartialEq, Default)]
pub struct RuleHeader {
pub action: String,
pub proto: String,
pub src_addr: String,
pub src_port: String,
pub direction: String,
pub dst_addr: String,
pub dst_port: String,
}
#[derive(Debug, PartialEq)]
pub struct RuleOption {
pub key: String,
pub val: Option<String>,
pub prefix: Option<String>,
}
#[derive(Debug)]
pub struct TokenizedRule {
pub disabled: bool,
pub header: RuleHeader,
pub options: Vec<RuleOption>,
pub original: String,
}
fn parse_header_token(input: &str) -> IResult<&str, &str, InternalError<&str>> {
preceded(multispace0, nom::bytes::complete::is_not(WHITESPACE))(input)
}
fn get_option_key(input: &str) -> IResult<&str, &str, InternalError<&str>> {
preceded(multispace0, nom::bytes::complete::is_not(";:"))(input)
}
fn get_option_value(input: &str) -> IResult<&str, String, InternalError<&str>> {
let mut output = Vec::new();
let mut escaped = false;
let mut end = 0;
#[allow(clippy::branches_sharing_code)]
for (i, c) in input.chars().enumerate() {
end = i;
if c == '\\' {
escaped = true;
} else if escaped {
if c == ';' {
output.push(c);
} else {
output.push('\\');
output.push(c);
}
escaped = false;
} else if c == ';' {
end += 1;
break;
} else {
output.push(c);
}
}
let (_, rem) = input.split_at(end);
Ok((rem, output.into_iter().collect()))
}
fn parse_list_token(input: &str) -> IResult<&str, &str, InternalError<&str>> {
let input = preceded(
multispace0,
nom::combinator::peek(nom::bytes::complete::tag("[")),
)(input)?
.0;
let mut in_list = 0;
let mut end = 0;
for (i, c) in input.chars().enumerate() {
match c {
'[' => in_list += 1,
']' => in_list -= 1,
_ => {}
}
if in_list == 0 {
end = i + 1;
break;
}
}
if in_list > 0 {
return Err(nom::Err::Error(InternalError::NoListEnd));
}
let (list, rem) = input.split_at(end);
Ok((rem, list))
}
fn parse_header(input: &str) -> IResult<&str, RuleHeader, InternalError<&str>> {
let maybe_list = &nom::branch::alt((parse_list_token, parse_header_token));
let (rem, (action, proto, src_addr, src_port, direction, dst_addr, dst_port)) = tuple((
parse_header_token,
maybe_list,
maybe_list,
maybe_list,
parse_header_token,
maybe_list,
maybe_list,
))(input)?;
Ok((
rem,
RuleHeader {
action: String::from(action),
proto: String::from(proto),
src_addr: String::from(src_addr),
src_port: String::from(src_port),
direction: String::from(direction),
dst_addr: String::from(dst_addr),
dst_port: String::from(dst_port),
},
))
}
fn parse_option(input: &str) -> IResult<&str, RuleOption, InternalError<&str>> {
let (input, _) = multispace0(input)?;
let (input, key) = get_option_key(input)?;
let (input, sep) = nom::character::complete::one_of(";:")(input)?;
if sep == ';' {
return Ok((
input,
RuleOption {
key: String::from(key),
val: None,
prefix: None,
},
));
}
let (input, _) = multispace0(input)?;
let (input, val) = get_option_value(input)?;
let (prefix, val) = strip_quotes(&val);
Ok((
input,
RuleOption {
key: String::from(key),
val: Some(val),
prefix,
},
))
}
fn strip_quotes(input: &str) -> (Option<String>, String) {
let mut escaped = false;
let mut prefix = None;
let mut out: Vec<char> = Vec::new();
let mut count = 0;
for c in input.chars() {
match c {
'"' if escaped => {
out.push(c);
escaped = false;
}
'"' => {
if count == 0 && !out.is_empty() {
prefix = Some(out.iter().collect());
out.truncate(0);
}
count += 1;
}
'\\' => {
escaped = true;
}
_ => {
if escaped {
out.push('\\');
escaped = false;
}
out.push(c);
}
}
}
(prefix, out.iter().collect())
}
fn internal_parse_rule(input: &str) -> IResult<&str, TokenizedRule, InternalError<&str>> {
let original = String::from(input);
let (input, disabled) = preceded(multispace0, many0_count(tag("#")))(input)?;
let (input, header) = parse_header(input)?;
let (input, _) = preceded(multispace0, tag("("))(input)?;
let (input, options) = many0(parse_option)(input)?;
let (input, _) = preceded(multispace0, tag(")"))(input)?;
Ok((
input,
TokenizedRule {
disabled: disabled > 0,
header,
options,
original,
},
))
}
pub fn parse_rule(input: &str) -> anyhow::Result<TokenizedRule> {
match internal_parse_rule(input) {
Ok((_, rule)) => Ok(rule),
Err(err) => Err(anyhow::anyhow!(err.to_string())),
}
}
pub fn read_next_rule(input: &mut dyn BufRead) -> Result<Option<String>, std::io::Error> {
let mut line = String::new();
loop {
let mut tmp = String::new();
let n = input.read_line(&mut tmp)?;
if n == 0 {
return Ok(None);
}
let tmp = tmp.trim();
if !tmp.ends_with('\\') {
line.push_str(tmp);
break;
}
line.push_str(&tmp[..tmp.len() - 1]);
}
Ok(Some(line))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_token() {
assert_eq!(parse_header_token("alert"), Ok(("", "alert")));
assert_eq!(parse_header_token(" alert"), Ok(("", "alert")));
assert_eq!(parse_header_token(" alert "), Ok((" ", "alert")));
assert_eq!(parse_header_token("http_uri"), Ok(("", "http_uri")));
}
#[test]
fn test_parse_quoted_string() {
assert_eq!(
strip_quotes(r#""some quoted \" string""#),
(None, r#"some quoted " string"#.to_string())
);
}
#[test]
fn test_parse_list() {
assert_eq!(parse_list_token("[1]"), Ok(("", "[1]")));
assert_eq!(parse_list_token("[1,2,3]"), Ok(("", "[1,2,3]")));
assert_eq!(parse_list_token(" [1,2,3]"), Ok(("", "[1,2,3]")));
assert_eq!(
parse_list_token(" [1,2,3,[a,b,c]]"),
Ok(("", "[1,2,3,[a,b,c]]"))
);
assert!(parse_list_token("1,2,3]").is_err());
assert!(parse_list_token("[1,2,3").is_err());
assert!(parse_list_token("token").is_err());
}
#[test]
fn test_parse_header() {
let rule = parse_header("alert tcp any any -> any any");
assert_eq!(
rule,
Ok((
"",
RuleHeader {
action: String::from("alert"),
proto: String::from("tcp"),
src_addr: String::from("any"),
src_port: String::from("any"),
direction: String::from("->"),
dst_addr: String::from("any"),
dst_port: String::from("any"),
..Default::default()
}
))
);
}
#[test]
fn test_parse_option_without_value() {
assert_eq!(
Ok((
"",
RuleOption {
key: String::from("http_uri"),
val: None,
prefix: None,
}
)),
parse_option("http_uri;")
);
}
#[test]
fn test_parse_negated_content() {
assert_eq!(
parse_option("content:!\"evebox\\\"\";"),
Ok((
"",
RuleOption {
key: String::from("content"),
val: Some("evebox\"".to_string()),
prefix: Some("!".to_string()),
}
))
);
}
#[test]
fn test_parse_option() {
assert_eq!(
parse_option("msg:value;"),
Ok((
"",
RuleOption {
key: String::from("msg"),
val: Some("value".to_string()),
prefix: None,
}
))
);
assert_eq!(
parse_option("msg:value with spaces;"),
Ok((
"",
RuleOption {
key: String::from("msg"),
val: Some("value with spaces".to_string()),
prefix: None,
}
))
);
assert_eq!(
parse_option("msg:terminated value with spaces;"),
Ok((
"",
RuleOption {
key: String::from("msg"),
val: Some("terminated value with spaces".to_string()),
prefix: None,
}
))
);
assert_eq!(
parse_option(r#"msg: an escaped \; terminant; next_option;"#),
Ok((
" next_option;",
RuleOption {
key: String::from("msg"),
val: Some("an escaped ; terminant".to_string()),
prefix: None,
}
))
);
assert_eq!(
parse_option(r#"msg: "A Quoted Message";"#),
Ok((
"",
RuleOption {
key: String::from("msg"),
val: Some(r#"A Quoted Message"#.to_string()),
prefix: None,
}
))
);
assert_eq!(
parse_option(r#"msg: "A Quoted Message with escaped \" quotes.";"#),
Ok((
"",
RuleOption {
key: String::from("msg"),
val: Some(r#"A Quoted Message with escaped " quotes."#.to_string()),
prefix: None,
}
))
);
assert_eq!(
parse_option(r#"pcre:"/^/index\.html/$/U";"#),
Ok((
"",
RuleOption {
key: String::from("pcre"),
val: Some(r#"/^/index\.html/$/U"#.to_string()),
prefix: None,
}
))
);
}
#[test]
fn test_parse_rule() {
let rule = parse_rule(r#"alert ip any any -> any any (msg:"Some Message with a \" Quote"; metadata: key, val; sid:1; rev:1;)"#).unwrap();
assert_eq!(rule.disabled, false);
assert_eq!(rule.header.action, "alert");
let rule = parse_rule(r#" alert ip any any -> any any (msg:"Some Message with a \" Quote"; metadata: key, val; sid:1; rev:1;)"#).unwrap();
assert_eq!(rule.disabled, false);
assert_eq!(rule.header.action, "alert");
let rule = parse_rule(r#"#alert ip any any -> any any (msg:"Some Message with a \" Quote"; metadata: key, val; sid:1; rev:1;)"#).unwrap();
assert_eq!(rule.disabled, true);
assert_eq!(rule.header.action, "alert");
let rule = parse_rule(r#"# alert ip any any -> any any (msg:"Some Message with a \" Quote"; metadata: key, val; sid:1; rev:1;)"#).unwrap();
assert_eq!(rule.disabled, true);
assert_eq!(rule.header.action, "alert");
let rule = parse_rule(r#"### alert ip any any -> any any (msg:"Some Message with a \" Quote"; metadata: key, val; sid:1; rev:1;)"#).unwrap();
assert_eq!(rule.disabled, true);
assert_eq!(rule.header.action, "alert");
}
#[test]
fn test_tokenize_option_value() {
assert_eq!(
get_option_value("one; two"),
Ok((" two", String::from("one")))
);
assert_eq!(
get_option_value("one\\;; two"),
Ok((" two", String::from("one;")))
);
let optval = r#""/^(?:[a-zA-Z0-9_%+])*(?:[\x2c\x22\x27\x28]|\x252[c278])/PRi";"#;
let expected = r#""/^(?:[a-zA-Z0-9_%+])*(?:[\x2c\x22\x27\x28]|\x252[c278])/PRi""#;
assert_eq!(get_option_value(optval), Ok(("", String::from(expected))));
}
#[test]
fn test_parse_from_reader() {
let input = r#"alert ip any any -> any any (\
msg:"TEST RULE"; sid:1; rev:1;)"#;
let mut reader = input.as_bytes();
let next = read_next_rule(&mut reader).unwrap();
assert_eq!(
next,
Some(r#"alert ip any any -> any any (msg:"TEST RULE"; sid:1; rev:1;)"#.to_string())
);
assert_eq!(read_next_rule(&mut reader).unwrap(), None);
}
}