santh-sigma 0.1.4

Sigma rule compiler for security research
Documentation
use secir::{Error, Result};
use super::ConditionExpr;

pub(crate) struct ConditionParser {
    pub(crate) tokens: Vec<String>,
    pub(crate) index: usize,
    pub(crate) depth: usize,
}

const MAX_CONDITION_DEPTH: usize = 64;

pub(crate) fn tokenize_condition(input: &str) -> Vec<String> {
    let mut tokens = Vec::new();
    let mut current = String::new();

    let flush = |current: &mut String, tokens: &mut Vec<String>| {
        let trimmed = current.trim().to_string();
        if !trimmed.is_empty() {
            tokens.push(trimmed);
        }
        current.clear();
    };

    let chars: Vec<char> = input.chars().collect();
    let mut i = 0;
    while i < chars.len() {
        let ch = chars[i];
        match ch {
            '(' | ')' | '|' | ',' => {
                flush(&mut current, &mut tokens);
                tokens.push(ch.to_string());
            }
            '>' | '<' | '=' => {
                flush(&mut current, &mut tokens);
                if i + 1 < chars.len() && chars[i + 1] == '=' {
                    tokens.push(format!("{}{}", ch, chars[i + 1]));
                    i += 1;
                } else {
                    tokens.push(ch.to_string());
                }
            }
            c if c.is_whitespace() => {
                flush(&mut current, &mut tokens);
            }
            _ => current.push(ch),
        }
        i += 1;
    }

    flush(&mut current, &mut tokens);
    tokens
}

impl ConditionParser {
    pub(crate) fn parse_expr(&mut self) -> Result<ConditionExpr> {
        if self.depth >= MAX_CONDITION_DEPTH {
            return Err(Error::TemplateValidation {
                id: "sigma-condition".to_string(),
                reason: format!(
                    "condition nesting exceeds limit of {MAX_CONDITION_DEPTH}. \
                     Fix: simplify the condition expression."
                ),
            });
        }
        self.depth += 1;
        let result = self.parse_expr_inner();
        self.depth -= 1;
        result
    }

    fn parse_expr_inner(&mut self) -> Result<ConditionExpr> {
        let mut expr = self.parse_and()?;
        while self.peek_keyword("|") {
            self.index += 1;
            if self.peek_keyword("near") {
                self.index += 1;
                let right = self.parse_and()?;
                expr = ConditionExpr::Near(Box::new(expr), Box::new(right));
            } else if self.peek_keyword("count") {
                return Err(Error::TemplateValidation {
                    id: "sigma-condition".to_string(),
                    reason: "Aggregation functions like count() are not supported yet. Fix: remove aggregation from the rule.".to_string(),
                });
            } else {
                break;
            }
        }
        Ok(expr)
    }

    fn parse_and(&mut self) -> Result<ConditionExpr> {
        let mut expr = self.parse_or()?;
        while self.peek_keyword("and") {
            self.index += 1;
            let right = self.parse_or()?;
            expr = ConditionExpr::And(Box::new(expr), Box::new(right));
        }
        Ok(expr)
    }

    fn parse_or(&mut self) -> Result<ConditionExpr> {
        let mut expr = self.parse_not()?;
        while self.peek_keyword("or") {
            self.index += 1;
            let right = self.parse_not()?;
            expr = ConditionExpr::Or(Box::new(expr), Box::new(right));
        }
        Ok(expr)
    }

    fn parse_not(&mut self) -> Result<ConditionExpr> {
        if self.peek_keyword("not") {
            self.index += 1;
            let inner = self.parse_primary()?;
            return Ok(ConditionExpr::Not(Box::new(inner)));
        }
        self.parse_primary()
    }

    fn parse_primary(&mut self) -> Result<ConditionExpr> {
        if let Some(of_expr) = self.parse_of_expression()? {
            return Ok(of_expr);
        }

        let token = self
            .tokens
            .get(self.index)
            .ok_or_else(|| Error::TemplateValidation {
                id: "sigma-condition".to_string(),
                reason: "unexpected end of condition".to_string(),
            })?;

        if token == "(" {
            self.index += 1;
            let expr = self.parse_expr()?;
            let closing = self
                .tokens
                .get(self.index)
                .ok_or_else(|| Error::TemplateValidation {
                    id: "sigma-condition".to_string(),
                    reason: "missing ')' in condition".to_string(),
                })?;
            if closing != ")" {
                return Err(Error::TemplateValidation {
                    id: "sigma-condition".to_string(),
                    reason: "missing ')' in condition".to_string(),
                });
            }
            self.index += 1;
            return Ok(expr);
        }

        if matches!(
            token.to_ascii_lowercase().as_str(),
            "and" | "or" | "not" | ")"
        ) {
            return Err(Error::TemplateValidation {
                id: "sigma-condition".to_string(),
                reason: format!("unexpected token '{}'", token),
            });
        }

        self.index += 1;
        Ok(ConditionExpr::Selection(token.clone()))
    }

    fn parse_of_expression(&mut self) -> Result<Option<ConditionExpr>> {
        if self.index >= self.tokens.len() {
            return Ok(None);
        }

        let (count_token, offset) = if self.peek_keyword("at")
            && self
                .tokens
                .get(self.index + 1)
                .is_some_and(|t| t.eq_ignore_ascii_case("least"))
        {
            let token = self.tokens.get(self.index + 2).map(|s| s.to_ascii_lowercase());
            (token, 3)
        } else {
            (Some(self.tokens[self.index].to_ascii_lowercase()), 1)
        };

        let count = match count_token.as_deref() {
            Some("all") => None,
            Some("any") => Some(1),
            Some(s) => {
                if let Ok(value) = s.parse::<usize>() {
                    Some(value)
                } else {
                    return Ok(None);
                }
            }
            None => return Ok(None),
        };

        if !self
            .tokens
            .get(self.index + offset)
            .is_some_and(|token| token.eq_ignore_ascii_case("of"))
        {
            return Ok(None);
        }

        let pattern_start = self.index + offset + 1;
        let (pattern, consumed) = if self
            .tokens
            .get(pattern_start)
            .is_some_and(|t| t == "(")
        {
            let mut parts = Vec::new();
            let mut j = pattern_start + 1;
            while j < self.tokens.len() && self.tokens[j] != ")" {
                if self.tokens[j] != "," {
                    parts.push(self.tokens[j].as_str());
                }
                j += 1;
            }
            if j >= self.tokens.len() {
                return Err(Error::TemplateValidation {
                    id: "sigma-condition".to_string(),
                    reason: "missing ')' in 'of' expression pattern".to_string(),
                });
            }
            if parts.is_empty() {
                return Err(Error::TemplateValidation {
                    id: "sigma-condition".to_string(),
                    reason: "empty pattern inside 'of' expression".to_string(),
                });
            }
            (parts.join(","), j - pattern_start + 1)
        } else if let Some(p) = self.tokens.get(pattern_start) {
            (p.clone(), 1)
        } else {
            return Err(Error::TemplateValidation {
                id: "sigma-condition".to_string(),
                reason: "unexpected end of condition in 'of' expression".to_string(),
            });
        };

        self.index = pattern_start + consumed;
        Ok(Some(ConditionExpr::Of { count, pattern }))
    }

    fn peek_keyword(&self, keyword: &str) -> bool {
        self.tokens
            .get(self.index)
            .map(|token| token.eq_ignore_ascii_case(keyword))
            .unwrap_or(false)
    }
}