use std::collections::HashMap;
use rsigma_eval::pipeline::{Pipeline, apply_pipelines_to_correlation, apply_pipelines_with_state};
use rsigma_parser::SigmaCollection;
use crate::backend::Backend;
use crate::error::{ConvertError, Result};
use crate::output::{ConversionOutput, ConversionResult};
pub fn convert_collection(
backend: &dyn Backend,
collection: &SigmaCollection,
pipelines: &[Pipeline],
output_format: &str,
) -> Result<ConversionOutput> {
if backend.requires_pipeline() && pipelines.is_empty() {
return Err(ConvertError::PipelineRequired);
}
let mut output = ConversionOutput::new();
let mut rule_table_map: HashMap<String, String> = HashMap::new();
let mut rule_schema_map: HashMap<String, String> = HashMap::new();
let mut rule_query_map: HashMap<String, String> = HashMap::new();
for rule in &collection.rules {
let mut rule = rule.clone();
let pipeline_state = if !pipelines.is_empty() {
apply_pipelines_with_state(pipelines, &mut rule)?
} else {
Default::default()
};
let resolved_table = rule
.custom_attributes
.get("postgres.table")
.and_then(|v| v.as_str())
.or_else(|| pipeline_state.state.get("table").and_then(|v| v.as_str()));
if let Some(table) = resolved_table {
if let Some(id) = &rule.id {
rule_table_map.insert(id.clone(), table.to_string());
}
rule_table_map.insert(rule.title.clone(), table.to_string());
}
let resolved_schema = rule
.custom_attributes
.get("postgres.schema")
.and_then(|v| v.as_str())
.or_else(|| pipeline_state.state.get("schema").and_then(|v| v.as_str()));
if let Some(schema) = resolved_schema {
if let Some(id) = &rule.id {
rule_schema_map.insert(id.clone(), schema.to_string());
}
rule_schema_map.insert(rule.title.clone(), schema.to_string());
}
match backend.convert_rule(&rule, output_format, &pipeline_state) {
Ok(queries) => {
if let Some(q) = queries.first() {
if let Some(id) = &rule.id {
rule_query_map.insert(id.clone(), q.clone());
}
rule_query_map.insert(rule.title.clone(), q.clone());
}
output.queries.push(ConversionResult {
rule_title: rule.title.clone(),
rule_id: rule.id.clone(),
queries,
warnings: Vec::new(),
});
}
Err(e) => {
output.errors.push((rule.title.clone(), e));
}
}
}
if backend.supports_correlation() {
for corr in &collection.correlations {
let mut corr = corr.clone();
let mut pipeline_state = if !pipelines.is_empty() {
apply_pipelines_to_correlation(pipelines, &mut corr)?
} else {
Default::default()
};
if !rule_table_map.is_empty() {
let map_value = serde_json::to_value(&rule_table_map)
.unwrap_or(serde_json::Value::Object(Default::default()));
pipeline_state.set_state("_rule_tables".to_string(), map_value);
}
if !rule_schema_map.is_empty() {
let map_value = serde_json::to_value(&rule_schema_map)
.unwrap_or(serde_json::Value::Object(Default::default()));
pipeline_state.set_state("_rule_schemas".to_string(), map_value);
}
if !rule_query_map.is_empty() {
let map_value = serde_json::to_value(&rule_query_map)
.unwrap_or(serde_json::Value::Object(Default::default()));
pipeline_state.set_state("_rule_queries".to_string(), map_value);
}
let mut warnings = Vec::new();
match backend.convert_correlation_rule_with_warnings(
&corr,
output_format,
&pipeline_state,
&mut warnings,
) {
Ok(queries) => {
output.queries.push(ConversionResult {
rule_title: corr.title.clone(),
rule_id: corr.id.clone(),
queries,
warnings,
});
}
Err(e) => {
output.errors.push((corr.title.clone(), e));
}
}
}
}
Ok(output)
}
pub(crate) fn field_has_positional_index(field: &str) -> bool {
field.split('.').any(|seg| {
let Some(open) = rsigma_parser::fieldpath::first_unescaped(seg, b'[') else {
return false;
};
if !rsigma_parser::fieldpath::ends_with_unescaped(seg, b']') {
return false;
}
let inner = &seg[open + 1..seg.len() - 1];
let digits = inner.strip_prefix('-').unwrap_or(inner);
!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit())
})
}
#[cfg(test)]
mod tests {
use super::field_has_positional_index;
#[test]
fn positional_index_detection_respects_escaping() {
assert!(field_has_positional_index("args[0]"));
assert!(field_has_positional_index("args[-1]"));
assert!(field_has_positional_index("connections[0].ip"));
assert!(!field_has_positional_index("args\\[0\\]"));
assert!(!field_has_positional_index("weird\\[x\\]"));
assert!(!field_has_positional_index("process.args"));
}
}