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_parser::*;
use crate::backend::*;
use crate::condition::convert_condition_expr;
use crate::convert::{default_convert_detection, default_convert_detection_item};
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} != false"),
field_not_exists_expression: Some("{field} = false"),
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: &[],
};
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 all_cased(values: &[&SigmaValue]) -> bool {
let _ = values;
false
}
fn try_string_value_list(&self, item: &DetectionItem) -> Result<Option<String>> {
if item.values.len() < 2 || item.field.has_modifier(Modifier::All) {
return Ok(None);
}
const ALLOWED: &[Modifier] = &[
Modifier::Contains,
Modifier::StartsWith,
Modifier::EndsWith,
Modifier::Cased,
];
if item.field.modifiers.iter().any(|m| !ALLOWED.contains(m)) {
return Ok(None);
}
let mut strings: Vec<&SigmaString> = Vec::with_capacity(item.values.len());
for v in &item.values {
match v {
SigmaValue::String(s) => strings.push(s),
_ => return Ok(None),
}
}
let field_name = item
.field
.name
.as_deref()
.ok_or(ConvertError::MissingFieldName)?;
let f = self.escape_and_quote_field(field_name);
let cased = self.fibratus.case_sensitive || item.field.has_modifier(Modifier::Cased);
let any_wild = strings.iter().any(|s| shared::has_wildcards(s));
let op = if item.field.has_modifier(Modifier::Contains) {
if cased { "contains" } else { "icontains" }
} else if item.field.has_modifier(Modifier::StartsWith) {
if cased { "startswith" } else { "istartswith" }
} else if item.field.has_modifier(Modifier::EndsWith) {
if cased { "endswith" } else { "iendswith" }
} else if any_wild {
if cased { "matches" } else { "imatches" }
} else if cased || field_name == "evt.name" {
"in"
} else {
"iin"
};
let list = strings
.iter()
.map(|s| shared::quote_sigma_string(s))
.collect::<Vec<_>>()
.join(", ");
Ok(Some(format!("{f} {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>> {
let mut queries = Vec::with_capacity(rule.detection.conditions.len());
for (idx, cond_expr) in rule.detection.conditions.iter().enumerate() {
let mut state = ConversionState::new(pipeline_state.state.clone());
let query = self.convert_condition(cond_expr, &rule.detection.named, &mut state)?;
let finished = self.finish_query(rule, query, &state)?;
let finalized = self.finalize_query(rule, finished, idx, &state, output_format)?;
queries.push(finalized);
}
Ok(queries)
}
fn convert_condition(
&self,
expr: &ConditionExpr,
detections: &HashMap<String, Detection>,
state: &mut ConversionState,
) -> Result<String> {
convert_condition_expr(self, expr, detections, 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_detection(&self, det: &Detection, state: &mut ConversionState) -> Result<String> {
default_convert_detection(self, det, state)
}
fn convert_detection_item(
&self,
item: &DetectionItem,
state: &mut ConversionState,
) -> Result<String> {
if item.field.has_modifier(Modifier::Re)
&& item.values.len() >= 2
&& !item.field.has_modifier(Modifier::All)
{
let field_name = item
.field
.name
.as_deref()
.ok_or(ConvertError::MissingFieldName)?;
let mut patterns: Vec<String> = Vec::with_capacity(item.values.len());
for v in &item.values {
let pat = match v {
SigmaValue::String(s) => s.original.clone(),
_ => return Err(ConvertError::UnsupportedValue("re requires string".into())),
};
if !shared::is_re2_compatible(&pat) {
return Err(ConvertError::UnsupportedModifier(format!(
"regex pattern uses PCRE-only construct (lookaround/backreference) Fibratus's RE2 engine does not support: {pat}"
)));
}
patterns.push(pat);
}
let f = self.escape_and_quote_field(field_name);
let quoted: Vec<String> = patterns
.iter()
.map(|p| shared::quote_plain_str(p))
.collect();
return Ok(format!("regex({f}, {}) = true", quoted.join(", ")));
}
if item.field.has_modifier(Modifier::Cidr)
&& item.values.len() >= 2
&& !item.field.has_modifier(Modifier::All)
{
let field_name = item
.field
.name
.as_deref()
.ok_or(ConvertError::MissingFieldName)?;
let mut masks: Vec<String> = Vec::with_capacity(item.values.len());
for v in &item.values {
match v {
SigmaValue::String(s) => masks.push(shared::quote_plain_str(&s.original)),
_ => {
return Err(ConvertError::UnsupportedValue(
"cidr requires string".into(),
));
}
}
}
let f = self.escape_and_quote_field(field_name);
return Ok(format!("cidr_contains({f}, {})", masks.join(", ")));
}
if let Some(list_expr) = self.try_string_value_list(item)? {
return Ok(list_expr);
}
default_convert_detection_item(self, item, state)
}
fn escape_and_quote_field(&self, field: &str) -> String {
shared::sanitize_field(field)
}
fn convert_value_str(&self, value: &SigmaString, _state: &ConversionState) -> String {
shared::quote_sigma_string(value)
}
fn convert_value_re(&self, regex: &str, _state: &ConversionState) -> String {
shared::quote_plain_str(regex)
}
fn convert_field_eq_str(
&self,
field: &str,
value: &SigmaString,
modifiers: &[Modifier],
state: &mut ConversionState,
) -> Result<ConvertResult> {
let mut mods = modifiers.to_vec();
if self.fibratus.case_sensitive && !mods.contains(&Modifier::Cased) {
mods.push(Modifier::Cased);
}
let f = self.escape_and_quote_field(field);
let val = self.convert_value_str(value, state);
let is_cased = mods.contains(&Modifier::Cased);
let is_contains = mods.contains(&Modifier::Contains);
let is_startswith = mods.contains(&Modifier::StartsWith);
let is_endswith = mods.contains(&Modifier::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 shared::has_wildcards(value) {
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_str_case_sensitive(
&self,
field: &str,
value: &SigmaString,
modifiers: &[Modifier],
state: &mut ConversionState,
) -> Result<ConvertResult> {
let mut mods = modifiers.to_vec();
if !mods.contains(&Modifier::Cased) {
mods.push(Modifier::Cased);
}
self.convert_field_eq_str(field, value, &mods, state)
}
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_eq_re(
&self,
field: &str,
pattern: &str,
_flags: &[Modifier],
_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}"
)));
}
let f = self.escape_and_quote_field(field);
let quoted = shared::quote_plain_str(pattern);
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(
&self,
field: &str,
op: &Modifier,
value: f64,
_state: &mut ConversionState,
) -> Result<String> {
let f = self.escape_and_quote_field(field);
let op_name = match op {
Modifier::Lt => "lt",
Modifier::Lte => "lte",
Modifier::Gt => "gt",
Modifier::Gte => "gte",
_ => {
return Err(ConvertError::UnsupportedModifier(format!(
"compare op {op:?}"
)));
}
};
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,
_state: &mut ConversionState,
) -> Result<ConvertResult> {
let f1 = self.escape_and_quote_field(field1);
let f2 = self.escape_and_quote_field(field2);
Ok(ConvertResult::Query(format!("{f1} = {f2}")))
}
fn convert_keyword(&self, _value: &SigmaValue, _state: &mut ConversionState) -> Result<String> {
Err(ConvertError::UnsupportedKeyword)
}
fn convert_condition_as_in_expression(
&self,
field: &str,
values: &[&SigmaValue],
is_or: bool,
_state: &mut ConversionState,
) -> Result<String> {
if !is_or {
return Err(ConvertError::UnsupportedModifier(
"and-in (all values present in a field) is not expressible as a single Fibratus operator".into(),
));
}
let f = self.escape_and_quote_field(field);
let op = if self.fibratus.case_sensitive || Self::all_cased(values) {
"in"
} else {
self.config.or_in_operator.unwrap_or("iin")
};
let expr = self
.config
.field_in_list_expression
.ok_or_else(|| ConvertError::UnsupportedModifier("in-list".into()))?;
let items: Vec<String> = values
.iter()
.map(|v| match v {
SigmaValue::String(s) => shared::quote_sigma_string(s),
SigmaValue::Integer(n) => n.to_string(),
SigmaValue::Float(f) => f.to_string(),
SigmaValue::Bool(b) => {
if *b {
self.config.bool_true.to_string()
} else {
self.config.bool_false.to_string()
}
}
SigmaValue::Null => "null".to_string(),
})
.collect();
let list = items.join(self.config.list_separator);
Ok(expr
.replace("{field}", &f)
.replace("{op}", op)
.replace("{list}", &list))
}
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 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)
}
}