use crate::theme::tokens;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleDef {
#[serde(default)]
pub name: String,
pub pattern: String,
#[serde(default)]
pub token: Option<String>,
#[serde(default)]
pub ignore_case: bool,
}
impl RuleDef {
pub fn new(name: &str, pattern: &str) -> RuleDef {
RuleDef { name: name.into(), pattern: pattern.into(), token: None, ignore_case: false }
}
pub fn with_token(name: &str, pattern: &str, token: &str) -> RuleDef {
RuleDef {
name: name.into(),
pattern: pattern.into(),
token: Some(token.into()),
ignore_case: false,
}
}
pub fn ci(mut self) -> RuleDef {
self.ignore_case = true;
self
}
}
#[derive(Debug, Clone)]
pub struct Rule {
pub name: String,
pub regex: Regex,
pub whole_token: String,
pub group_tokens: Vec<Option<String>>,
pub has_named_groups: bool,
}
impl Rule {
pub fn compile(def: &RuleDef) -> Result<Rule, crate::error::Error> {
let pat = if def.ignore_case {
format!("(?i){}", def.pattern)
} else {
def.pattern.clone()
};
let regex = Regex::new(&pat).map_err(|e| crate::error::Error::BadRule {
name: def.name.clone(),
source: e,
})?;
let group_tokens: Vec<Option<String>> =
regex.capture_names().map(|n| n.map(|s| s.to_string())).collect();
let has_named_groups = group_tokens.iter().skip(1).any(|n| n.is_some());
Ok(Rule {
name: def.name.clone(),
regex,
whole_token: def.token.clone().unwrap_or_else(|| tokens::DEFAULT.to_string()),
group_tokens,
has_named_groups,
})
}
}
pub fn compile_all(defs: &[RuleDef]) -> Result<Vec<Rule>, crate::error::Error> {
defs.iter().map(Rule::compile).collect()
}
#[allow(clippy::vec_init_then_push)] pub fn builtin_generic() -> Vec<RuleDef> {
use tokens::*;
let mut r = Vec::new();
r.push(RuleDef::new(
"iso-datetime",
r"(?P<date>\d{4}-\d{2}-\d{2})[T ](?P<time>\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)",
));
r.push(RuleDef::new(
"syslog-datetime",
r"(?P<date>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2})\s+(?P<time>\d{2}:\d{2}:\d{2})",
));
r.push(RuleDef::new("proc-pid", r"(?P<process>[\w./-]+)\[(?P<pid>\d+)\]"));
r.push(RuleDef::with_token("lvl-error", r"\b(ERROR|ERR|CRITICAL|CRIT|FATAL|EMERG|ALERT|PANIC)\b", ERROR).ci());
r.push(RuleDef::with_token("lvl-warn", r"\b(WARNING|WARN)\b", WARNING).ci());
r.push(RuleDef::with_token("lvl-debug", r"\b(DEBUG|TRACE)\b", DEBUG).ci());
r.push(RuleDef::with_token("lvl-info", r"\b(INFO|NOTICE)\b", INFO).ci());
r.push(RuleDef::with_token(
"bad-words",
r"\b(warn\w*|restart\w*|exit\w*|stop\w*|shut\w*|down|close\w*|unreach\w*|can'?t|cannot|skip\w*|den\w+|disabl\w+|ignor\w+|miss\w*|oops|fail\w*|unable|readonly|offline|terminat\w*|empty|virus|reject\w*|refus\w+|timed?\s?out|timeout)\b",
BAD,
).ci());
r.push(RuleDef::with_token(
"good-words",
r"\b(activ\w*|start\w*|read\w+|online|loaded|ok|success\w*|register\w*|detected|configured|enabl\w+|listen\w*|open\w*|complete\w*|done|connect\w*|finish\w*|clean|accept\w*|established|up)\b",
GOOD,
).ci());
r.push(RuleDef::with_token(
"system-words",
r"\b(ext[234]|reiserfs|xfs|btrfs|zfs|vfs|iso9?6?6?0?|isofs|ppp|bsd|linux|tcp/ip|mtrr|pci\w*|isa|scsi|ide|atapi|bios|cpu|fpu|kernel|systemd|udev|dbus)\b",
SYSTEM,
).ci());
r.push(RuleDef::with_token("http-method", r"\b(GET|POST|PUT|DELETE|HEAD|OPTIONS|PATCH|CONNECT|TRACE)\b", HTTP_METHOD));
r.push(RuleDef::new("http-code", r#"HTTP/\d\.\d"?\s+(?P<http_code>[1-5]\d{2})\b"#));
r.push(RuleDef::with_token("ipv4", r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", IP));
r.push(RuleDef::with_token("ipv6", r"\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b", IP));
r.push(RuleDef::with_token("mac", r"\b(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\b", MAC));
r.push(RuleDef::with_token("email", r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b", EMAIL));
r.push(RuleDef::with_token("uri", r#"\b[a-z][a-z0-9+.-]*://[^\s)\]}>'"]+"#, URI).ci());
r.push(RuleDef::with_token("hostname", r"\b(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}\b", HOST).ci());
r.push(RuleDef::with_token("abs-path", r"(?:^|\s)(?P<dir>/[\w./@+-]*)", DIR));
r.push(RuleDef::with_token("hex-addr", r"\b0x[0-9a-fA-F]+\b", ADDRESS));
r.push(RuleDef::with_token("size", r"\b\d+(?:\.\d+)?\s?(?:[KMGTP]i?B|[kmgtp]b?|bytes?)\b", SIZE));
r.push(RuleDef::with_token("version", r"\bv?\d+\.\d+(?:\.\d+)*(?:[-+][\w.]+)?\b", VERSION).ci());
r.push(RuleDef::with_token("percentage", r"\b\d+(?:\.\d+)?%", PERCENTAGE));
r.push(RuleDef::with_token("signal", r"\bSIG(?:HUP|INT|QUIT|ILL|TRAP|ABRT|BUS|FPE|KILL|USR1|SEGV|USR2|PIPE|ALRM|TERM|CHLD|CONT|STOP|TSTP|TTIN|TTOU)\b", SIGNAL));
r.push(RuleDef::with_token("number", r"\b\d+\b", NUMBER));
r.push(RuleDef::with_token("dq-string", r#""[^"]*""#, STRING));
r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_ruleset_compiles() {
let rules = compile_all(&builtin_generic()).expect("all builtin rules compile");
assert!(!rules.is_empty());
}
#[test]
fn named_groups_detected() {
let rule = Rule::compile(&RuleDef::new("dt", r"(?P<date>\d+)-(?P<time>\d+)")).unwrap();
assert!(rule.has_named_groups);
assert_eq!(rule.group_tokens, vec![None, Some("date".into()), Some("time".into())]);
}
#[test]
fn whole_match_default_token() {
let rule = Rule::compile(&RuleDef::with_token("e", r"ERROR", "error")).unwrap();
assert!(!rule.has_named_groups);
assert_eq!(rule.whole_token, "error");
}
}