pub mod config;
pub mod correlation;
pub mod envelope;
pub mod macros;
pub mod shared;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use rsigma_eval::pipeline::state::PipelineState;
use rsigma_ir::{IrDetectionItem, IrMatcher, IrPattern, IrPatternPart, IrStrOp};
use rsigma_parser::*;
use crate::backend::*;
use crate::condition_ir::convert_rule_via_ir;
use crate::error::{ConvertError, Result};
use crate::state::{ConversionState, ConvertResult};
pub use config::FibratusConfig;
pub static FIBRATUS_CONFIG: TextQueryConfig = TextQueryConfig {
precedence: (TokenType::NOT, TokenType::AND, TokenType::OR),
group_expression: "({expr})",
token_separator: " ",
and_token: "and",
or_token: "or",
not_token: "not",
eq_token: " = ",
not_eq_token: Some(" != "),
eq_expression: None,
not_eq_expression: None,
convert_not_as_not_eq: false,
wildcard_multi: "*",
wildcard_single: "?",
str_quote: "'",
str_quote_pattern: None,
str_quote_pattern_negation: false,
escape_char: "\\",
add_escaped: &[],
filter_chars: &[],
field_quote: None,
field_quote_pattern: None,
field_quote_pattern_negation: false,
field_escape: None,
field_escape_pattern: None,
startswith_expression: Some("{field} istartswith {value}"),
not_startswith_expression: None,
startswith_expression_allow_special: false,
endswith_expression: Some("{field} iendswith {value}"),
not_endswith_expression: None,
endswith_expression_allow_special: false,
contains_expression: Some("{field} icontains {value}"),
not_contains_expression: None,
contains_expression_allow_special: false,
wildcard_match_expression: Some("{field} imatches {value}"),
case_sensitive_match_expression: Some("{field} matches {value}"),
case_sensitive_startswith_expression: Some("{field} startswith {value}"),
case_sensitive_endswith_expression: Some("{field} endswith {value}"),
case_sensitive_contains_expression: Some("{field} contains {value}"),
re_expression: None,
not_re_expression: None,
re_escape_char: Some("\\"),
re_escape: &[],
re_escape_escape_char: None,
cidr_expression: None,
not_cidr_expression: None,
field_null_expression: "{field} = ''",
field_exists_expression: Some("{field} != ''"),
field_not_exists_expression: Some("{field} = ''"),
compare_op_expression: Some("{field} {op} {value}"),
compare_ops: &[("lt", "<"), ("lte", "<="), ("gt", ">"), ("gte", ">=")],
convert_or_as_in: true,
convert_and_as_in: false,
in_expressions_allow_wildcards: false,
field_in_list_expression: Some("{field} {op} ({list})"),
or_in_operator: Some("iin"),
and_in_operator: None,
list_separator: ", ",
unbound_value_str_expression: None,
unbound_value_num_expression: None,
unbound_value_re_expression: None,
field_eq_field_expression: Some("{field1} = {field2}"),
field_eq_field_escaping_quoting: false,
deferred_start: None,
deferred_separator: None,
deferred_only_query: "",
bool_true: "true",
bool_false: "false",
query_expression: "{query}",
state_defaults: &[],
};
fn has_literal_glob_char(pattern: &IrPattern) -> bool {
pattern
.parts
.iter()
.any(|p| matches!(p, IrPatternPart::Literal(s) if s.contains(['*', '?'])))
}
fn glob_regex(pattern: &IrPattern, op: IrStrOp, case_insensitive: bool) -> String {
let mut re = String::from(if case_insensitive { "(?is)" } else { "(?s)" });
if matches!(op, IrStrOp::Exact | IrStrOp::StartsWith) {
re.push('^');
}
for part in &pattern.parts {
match part {
IrPatternPart::Literal(s) => re.push_str(®ex::escape(s)),
IrPatternPart::WildcardMulti => re.push_str(".*"),
IrPatternPart::WildcardSingle => re.push('.'),
}
}
if matches!(op, IrStrOp::Exact | IrStrOp::EndsWith) {
re.push('$');
}
re
}
pub struct FibratusBackend {
pub config: &'static TextQueryConfig,
pub fibratus: FibratusConfig,
}
impl FibratusBackend {
pub fn new() -> Self {
Self {
config: &FIBRATUS_CONFIG,
fibratus: FibratusConfig::default(),
}
}
pub fn from_options(options: &HashMap<String, String>) -> Self {
Self {
config: &FIBRATUS_CONFIG,
fibratus: FibratusConfig::from_options(options),
}
}
fn try_string_value_list_ir(&self, field: &str, ms: &[IrMatcher]) -> Option<String> {
let mut op: Option<IrStrOp> = None;
let mut ci: Option<bool> = None;
let mut patterns = Vec::with_capacity(ms.len());
for m in ms {
match m {
IrMatcher::Str {
op: o,
pattern,
case_insensitive,
} => {
if *op.get_or_insert(*o) != *o {
return None;
}
if *ci.get_or_insert(*case_insensitive) != *case_insensitive {
return None;
}
if reject_nul_pattern("Fibratus", pattern).is_err() {
return None;
}
patterns.push(pattern);
}
_ => return None,
}
}
let op = op?;
let cased = self.fibratus.case_sensitive || !ci?;
let any_wild = patterns.iter().any(|p| p.has_wildcards());
if any_wild && (op != IrStrOp::Exact || patterns.iter().any(|p| has_literal_glob_char(p))) {
return None;
}
let f = self.escape_and_quote_field(field);
let list_op = match op {
IrStrOp::Contains => {
if cased {
"contains"
} else {
"icontains"
}
}
IrStrOp::StartsWith => {
if cased {
"startswith"
} else {
"istartswith"
}
}
IrStrOp::EndsWith => {
if cased {
"endswith"
} else {
"iendswith"
}
}
IrStrOp::Exact => {
if any_wild {
if cased { "matches" } else { "imatches" }
} else if cased || field == "evt.name" {
"in"
} else {
"iin"
}
}
};
let list = patterns
.iter()
.map(|p| shared::quote_sigma_string(&crate::backend::ir_pattern_to_sigma(p)))
.collect::<Vec<_>>()
.join(", ");
Some(format!("{f} {list_op} ({list})"))
}
}
impl Default for FibratusBackend {
fn default() -> Self {
Self::new()
}
}
impl Backend for FibratusBackend {
fn name(&self) -> &str {
"fibratus"
}
fn formats(&self) -> &[(&str, &str)] {
&[
(
"default",
"one YAML rule document per Sigma rule, --- separated",
),
("expr", "filter expression only, no YAML envelope"),
("yaml", "alias of `default`"),
("rule", "alias of `default`"),
]
}
fn requires_pipeline(&self) -> bool {
false
}
fn convert_rule(
&self,
rule: &SigmaRule,
output_format: &str,
pipeline_state: &PipelineState,
) -> Result<Vec<String>> {
convert_rule_via_ir(self, rule, output_format, pipeline_state)
}
fn convert_condition_and(&self, exprs: &[String]) -> Result<String> {
let non_empty: Vec<String> = exprs.iter().filter(|s| !s.is_empty()).cloned().collect();
if non_empty.is_empty() {
return Ok(String::new());
}
Ok(text_convert_condition_and(self.config, &non_empty))
}
fn convert_condition_or(&self, exprs: &[String]) -> Result<String> {
let non_empty: Vec<String> = exprs.iter().filter(|s| !s.is_empty()).cloned().collect();
if non_empty.is_empty() {
return Ok(String::new());
}
let joined = text_convert_condition_or(self.config, &non_empty);
if non_empty.len() > 1 {
Ok(format!("({joined})"))
} else {
Ok(joined)
}
}
fn convert_condition_not(&self, expr: &str) -> Result<String> {
if expr.is_empty() {
return Ok(String::new());
}
Ok(format!("not ({expr})"))
}
fn convert_condition_group(
&self,
expr: &str,
_outer: TokenType,
_inner: TokenType,
) -> Result<String> {
Ok(expr.to_string())
}
fn convert_ir_detection_item(
&self,
item: &IrDetectionItem,
state: &mut ConversionState,
) -> Result<String> {
if let IrMatcher::Not(inner) = &item.matcher
&& matches!(inner.as_ref(), IrMatcher::AnyOf(_) | IrMatcher::AllOf(_))
{
let positive = IrDetectionItem {
matcher: inner.as_ref().clone(),
..item.clone()
};
let expr = self.convert_ir_detection_item(&positive, state)?;
return self.convert_condition_not(&expr);
}
if let IrMatcher::AnyOf(ms) = &item.matcher
&& let Some(field) = item.field.as_deref()
&& ms.len() >= 2
{
if ms.iter().all(|m| matches!(m, IrMatcher::Regex { .. })) {
let mut quoted = Vec::with_capacity(ms.len());
for m in ms {
if let IrMatcher::Regex {
pattern,
case_insensitive,
multiline,
dotall,
cased,
} = m
{
if !shared::is_re2_compatible(pattern) {
return Err(ConvertError::UnsupportedModifier(format!(
"regex pattern uses PCRE-only construct (lookaround/backreference) Fibratus's RE2 engine does not support: {pattern}"
)));
}
reject_nul("Fibratus", pattern)?;
let flags = RegexFlags {
case_insensitive: *case_insensitive,
multiline: *multiline,
dotall: *dotall,
cased: *cased,
};
quoted.push(shared::quote_plain_str(&format!(
"{}{pattern}",
flags.inline_prefix()
)));
}
}
let f = self.escape_and_quote_field(field);
return Ok(format!("regex({f}, {}) = true", quoted.join(", ")));
}
if ms.iter().all(|m| matches!(m, IrMatcher::Cidr { .. })) {
let masks: Vec<String> = ms
.iter()
.filter_map(|m| match m {
IrMatcher::Cidr { network } => Some(shared::quote_plain_str(network)),
_ => None,
})
.collect();
let f = self.escape_and_quote_field(field);
return Ok(format!("cidr_contains({f}, {})", masks.join(", ")));
}
if let Some(list_expr) = self.try_string_value_list_ir(field, ms) {
return Ok(list_expr);
}
}
crate::ir_convert::default_convert_ir_detection_item(self, item, state)
}
fn escape_and_quote_field(&self, field: &str) -> String {
shared::sanitize_field(field)
}
fn convert_field_str(
&self,
field: &str,
op: IrStrOp,
pattern: &IrPattern,
case_insensitive: bool,
_state: &mut ConversionState,
) -> Result<ConvertResult> {
reject_nul_pattern("Fibratus", pattern)?;
let f = self.escape_and_quote_field(field);
let is_cased = self.fibratus.case_sensitive || !case_insensitive;
if pattern.has_wildcards() && has_literal_glob_char(pattern) {
let re = shared::quote_plain_str(&glob_regex(pattern, op, !is_cased));
return Ok(ConvertResult::Query(format!("regex({f}, {re}) = true")));
}
let globbed;
let (op, pattern) = if pattern.has_wildcards() && op != IrStrOp::Exact {
let mut parts = Vec::with_capacity(pattern.parts.len() + 2);
if matches!(op, IrStrOp::Contains | IrStrOp::EndsWith) {
parts.push(IrPatternPart::WildcardMulti);
}
parts.extend(pattern.parts.iter().cloned());
if matches!(op, IrStrOp::Contains | IrStrOp::StartsWith) {
parts.push(IrPatternPart::WildcardMulti);
}
globbed = IrPattern { parts };
(IrStrOp::Exact, &globbed)
} else {
(op, pattern)
};
let val = shared::quote_sigma_string(&crate::backend::ir_pattern_to_sigma(pattern));
let is_contains = matches!(op, IrStrOp::Contains);
let is_startswith = matches!(op, IrStrOp::StartsWith);
let is_endswith = matches!(op, IrStrOp::EndsWith);
if is_contains || is_startswith || is_endswith {
let template = match (is_cased, is_contains, is_startswith, is_endswith) {
(true, true, _, _) => self.config.case_sensitive_contains_expression,
(true, _, true, _) => self.config.case_sensitive_startswith_expression,
(true, _, _, true) => self.config.case_sensitive_endswith_expression,
(false, true, _, _) => self.config.contains_expression,
(false, _, true, _) => self.config.startswith_expression,
(false, _, _, true) => self.config.endswith_expression,
_ => unreachable!("substring branch guarded above"),
};
let expr = template.ok_or_else(|| {
ConvertError::UnsupportedModifier(format!("string operator for {field}"))
})?;
return Ok(ConvertResult::Query(
expr.replace("{field}", &f).replace("{value}", &val),
));
}
let expr = if pattern.has_wildcards() {
let template = if is_cased {
self.config.case_sensitive_match_expression
} else {
self.config.wildcard_match_expression
};
let template = template.ok_or_else(|| {
ConvertError::UnsupportedModifier(format!("string operator for {field}"))
})?;
template.replace("{field}", &f).replace("{value}", &val)
} else {
let op = if is_cased || field == "evt.name" {
"="
} else {
"~="
};
format!("{f} {op} {val}")
};
Ok(ConvertResult::Query(expr))
}
fn convert_field_eq_num(
&self,
field: &str,
value: f64,
_state: &mut ConversionState,
) -> Result<String> {
let f = self.escape_and_quote_field(field);
if value.fract() == 0.0 {
Ok(format!("{f} = {}", value as i64))
} else {
Ok(format!("{f} = {value}"))
}
}
fn convert_field_eq_bool(
&self,
field: &str,
value: bool,
_state: &mut ConversionState,
) -> Result<String> {
let f = self.escape_and_quote_field(field);
let v = if value {
self.config.bool_true
} else {
self.config.bool_false
};
Ok(format!("{f} = {v}"))
}
fn convert_field_eq_null(&self, field: &str, _state: &mut ConversionState) -> Result<String> {
let f = self.escape_and_quote_field(field);
Ok(self.config.field_null_expression.replace("{field}", &f))
}
fn convert_field_regex(
&self,
field: &str,
pattern: &str,
flags: RegexFlags,
_state: &mut ConversionState,
) -> Result<ConvertResult> {
if !shared::is_re2_compatible(pattern) {
return Err(ConvertError::UnsupportedModifier(format!(
"regex pattern uses PCRE-only construct (lookaround/backreference) Fibratus's RE2 engine does not support: {pattern}"
)));
}
reject_nul("Fibratus", pattern)?;
let f = self.escape_and_quote_field(field);
let quoted = shared::quote_plain_str(&format!("{}{pattern}", flags.inline_prefix()));
Ok(ConvertResult::Query(format!("regex({f}, {quoted}) = true")))
}
fn convert_field_eq_cidr(
&self,
field: &str,
cidr: &str,
_state: &mut ConversionState,
) -> Result<ConvertResult> {
let f = self.escape_and_quote_field(field);
let quoted = shared::quote_plain_str(cidr);
Ok(ConvertResult::Query(format!(
"cidr_contains({f}, {quoted})"
)))
}
fn convert_field_compare_op(
&self,
field: &str,
op: CompareOp,
value: f64,
_state: &mut ConversionState,
) -> Result<String> {
let f = self.escape_and_quote_field(field);
let op_name = match op {
CompareOp::Lt => "lt",
CompareOp::Lte => "lte",
CompareOp::Gt => "gt",
CompareOp::Gte => "gte",
};
let op_token = self
.config
.compare_ops
.iter()
.find(|(n, _)| *n == op_name)
.map(|(_, t)| *t)
.ok_or_else(|| ConvertError::UnsupportedModifier(op_name.into()))?;
let val_str = if value.fract() == 0.0 {
(value as i64).to_string()
} else {
value.to_string()
};
Ok(format!("{f} {op_token} {val_str}"))
}
fn convert_field_exists(
&self,
field: &str,
exists: bool,
_state: &mut ConversionState,
) -> Result<String> {
let f = self.escape_and_quote_field(field);
let template = if exists {
self.config.field_exists_expression
} else {
self.config.field_not_exists_expression
};
let expr = template.ok_or_else(|| {
ConvertError::UnsupportedModifier(if exists { "exists" } else { "not exists" }.into())
})?;
Ok(expr.replace("{field}", &f))
}
fn convert_field_eq_query_expr(
&self,
field: &str,
expr: &str,
_id: &str,
_state: &mut ConversionState,
) -> Result<String> {
let f = self.escape_and_quote_field(field);
Ok(format!("{f} = {expr}"))
}
fn convert_field_ref(
&self,
field1: &str,
field2: &str,
op: IrStrOp,
case_insensitive: bool,
_state: &mut ConversionState,
) -> Result<ConvertResult> {
let f1 = self.escape_and_quote_field(field1);
let f2 = self.escape_and_quote_field(field2);
let cased = self.fibratus.case_sensitive || !case_insensitive;
let token = match (op, cased) {
(IrStrOp::Exact, false) => "~=",
(IrStrOp::Exact, true) => "=",
(IrStrOp::Contains, false) => "icontains",
(IrStrOp::Contains, true) => "contains",
(IrStrOp::StartsWith, false) => "istartswith",
(IrStrOp::StartsWith, true) => "startswith",
(IrStrOp::EndsWith, false) => "iendswith",
(IrStrOp::EndsWith, true) => "endswith",
};
Ok(ConvertResult::Query(format!("{f1} {token} {f2}")))
}
fn convert_keyword_str(
&self,
_pattern: &IrPattern,
_state: &mut ConversionState,
) -> Result<String> {
Err(ConvertError::UnsupportedKeyword)
}
fn convert_keyword_num(&self, _value: f64, _state: &mut ConversionState) -> Result<String> {
Err(ConvertError::UnsupportedKeyword)
}
fn finish_query(
&self,
_rule: &SigmaRule,
query: String,
_state: &ConversionState,
) -> Result<String> {
Ok(query)
}
fn finalize_query(
&self,
rule: &SigmaRule,
query: String,
_index: usize,
_state: &ConversionState,
output_format: &str,
) -> Result<String> {
let condition = if self.fibratus.use_macros {
macros::recognize(&query)
} else {
query
};
match output_format {
"expr" => Ok(condition),
"default" | "yaml" | "rule" => {
Ok(envelope::render_rule_yaml(rule, &condition, &self.fibratus))
}
other => Err(ConvertError::RuleConversion(format!(
"unknown output format: {other}"
))),
}
}
fn output_file_extension(&self, output_format: &str) -> &str {
match output_format {
"expr" => "txt",
_ => "yml",
}
}
fn finalize_output(&self, queries: Vec<String>, output_format: &str) -> Result<String> {
match output_format {
"expr" => Ok(queries.join("\n")),
"default" | "yaml" | "rule" => {
let mut out = String::new();
for (i, q) in queries.iter().enumerate() {
if i > 0 {
out.push_str("---\n");
}
out.push_str(q);
}
Ok(out)
}
other => Err(ConvertError::RuleConversion(format!(
"unknown output format: {other}"
))),
}
}
fn supports_correlation(&self) -> bool {
true
}
fn correlation_methods(&self) -> &[(&str, &str)] {
&[
(
"sliding",
"Native sliding sequence with `maxspan` (default; the Fibratus sequence DSL's only \
time-window primitive is a total-span cap, which is a sliding constraint per stage)",
),
(
"session",
"Degraded: emits a sliding sequence and a warning that the requested per-step gap \
is not enforced (Fibratus has no `maxpause`-style inactivity timeout)",
),
]
}
fn default_correlation_method(&self) -> &str {
"sliding"
}
fn convert_correlation_rule_with_warnings(
&self,
rule: &CorrelationRule,
output_format: &str,
pipeline_state: &PipelineState,
warnings: &mut Vec<String>,
) -> Result<Vec<String>> {
correlation::convert(self, rule, output_format, pipeline_state, warnings)
}
}