santh-sigma 0.1.4

Sigma rule compiler for security research
Documentation
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![cfg_attr(
    not(test),
    deny(
        clippy::unwrap_used,
        clippy::expect_used,
        clippy::todo,
        clippy::unimplemented,
        clippy::panic
    )
)]
#![allow(
    clippy::module_name_repetitions,
    clippy::must_use_candidate,
    clippy::missing_errors_doc
)]
//! Sigma rule compilation for Calyx.
//!
//! Compiles Sigma detection rules (YAML) into the secir [`Template`]
//! intermediate representation, enabling Calyx to match Sigma rules against
//! log data alongside Nuclei HTTP templates.
//!
//! # Sigma Format Support
//!
//! - `title` → template ID (kebab-cased)
//! - `logsource` → preserved in tags and metadata
//! - `detection.selection*` → word/regex matchers with `|contains`,
//!   `|endswith`, `|startswith` modifiers
//! - `condition` → matcher condition logic (AND/OR between selections,
//!   aggregation piped expressions stored in metadata)
//! - `level` → severity mapping
//! - `timeframe` → stored in metadata for aggregation queries
//!
//! # Safe Defaults
//!
//! - Input size: Bounded in-memory YAML parsing with strict recursion and token limits.
//! - Recursion depth: Restricted expression parsing depth to prevent stack overflow.
//! - Outbound network: No network calls are executed by this compiler crate.
//! - Process spawning: No process spawning or child process execution.
//! - Filesystem writes: Pure in-memory transformation with zero file writes.
//! - Credential exposure: No credentials cached or exposed during compilation.

pub(crate) mod condition;
pub(crate) mod parse;
pub(crate) mod selection;

use parse::{SigmaRule, compile_rule};

pub(crate) fn escape_dsl_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 4);
    for ch in s.chars() {
        if ch == '"' || ch == '\\' {
            out.push('\\');
        }
        out.push(ch);
    }
    out
}

pub(crate) fn base64_offset(s: &str) -> Vec<String> {
    use base64::{Engine as _, engine::general_purpose};

    if s.is_empty() {
        return vec![];
    }

    let n = s.len();
    let mut variants = Vec::with_capacity(3);

    for i in 0usize..3 {
        let mut prefixed = vec![0u8; i];
        prefixed.extend_from_slice(s.as_bytes());
        let encoded = general_purpose::STANDARD.encode(&prefixed);

        let start = (i * 4 + 2) / 3;
        let end = ((i + n) * 4) / 3;

        if start < end {
            variants.push(encoded[start..end].to_string());
        }
    }
    variants
}

/// Sigma-to-secir compiler.
///
/// Parses Sigma YAML rules and produces secir [`secir::Template`] instances
/// suitable for compilation into the Calyx match database.
#[derive(Debug, Clone, Default)]
pub struct SigmaCompiler;

impl SigmaCompiler {
    /// Creates a new compiler instance.
    #[must_use]
    pub fn new() -> Self {
        Self
    }

    /// Compiles a Sigma YAML string into a secir template.
    ///
    /// # Errors
    ///
    /// Returns an error if the YAML cannot be parsed or the rule is invalid.
    pub fn compile(&self, yaml: &str) -> secir::Result<secir::Template> {
        let rule: SigmaRule =
            serde_yaml::from_str(yaml).map_err(|source| secir::Error::TemplateValidation {
                id: "sigma".to_string(),
                reason: format!("failed to parse sigma YAML: {source}"),
            })?;
        compile_rule(rule)
    }

    /// Compiles a Sigma YAML string into a secir template (static convenience method).
    ///
    /// # Errors
    ///
    /// Returns an error if the YAML cannot be parsed or the rule is invalid.
    pub fn compile_str(yaml: &str) -> secir::Result<secir::Template> {
        Self::new().compile(yaml)
    }
}