Skip to main content

sigma/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3#![warn(clippy::pedantic)]
4#![cfg_attr(
5    not(test),
6    deny(
7        clippy::unwrap_used,
8        clippy::expect_used,
9        clippy::todo,
10        clippy::unimplemented,
11        clippy::panic
12    )
13)]
14#![allow(
15    clippy::module_name_repetitions,
16    clippy::must_use_candidate,
17    clippy::missing_errors_doc
18)]
19//! Sigma rule compilation for Calyx.
20//!
21//! Compiles Sigma detection rules (YAML) into the secir [`Template`]
22//! intermediate representation, enabling Calyx to match Sigma rules against
23//! log data alongside Nuclei HTTP templates.
24//!
25//! # Sigma Format Support
26//!
27//! - `title` → template ID (kebab-cased)
28//! - `logsource` → preserved in tags and metadata
29//! - `detection.selection*` → word/regex matchers with `|contains`,
30//!   `|endswith`, `|startswith` modifiers
31//! - `condition` → matcher condition logic (AND/OR between selections,
32//!   aggregation piped expressions stored in metadata)
33//! - `level` → severity mapping
34//! - `timeframe` → stored in metadata for aggregation queries
35//!
36//! # Safe Defaults
37//!
38//! - Input size: Bounded in-memory YAML parsing with strict recursion and token limits.
39//! - Recursion depth: Restricted expression parsing depth to prevent stack overflow.
40//! - Outbound network: No network calls are executed by this compiler crate.
41//! - Process spawning: No process spawning or child process execution.
42//! - Filesystem writes: Pure in-memory transformation with zero file writes.
43//! - Credential exposure: No credentials cached or exposed during compilation.
44
45pub(crate) mod condition;
46pub(crate) mod parse;
47pub(crate) mod selection;
48
49use parse::{SigmaRule, compile_rule};
50
51pub(crate) fn escape_dsl_string(s: &str) -> String {
52    let mut out = String::with_capacity(s.len() + 4);
53    for ch in s.chars() {
54        if ch == '"' || ch == '\\' {
55            out.push('\\');
56        }
57        out.push(ch);
58    }
59    out
60}
61
62pub(crate) fn base64_offset(s: &str) -> Vec<String> {
63    use base64::{Engine as _, engine::general_purpose};
64
65    if s.is_empty() {
66        return vec![];
67    }
68
69    let n = s.len();
70    let mut variants = Vec::with_capacity(3);
71
72    for i in 0usize..3 {
73        let mut prefixed = vec![0u8; i];
74        prefixed.extend_from_slice(s.as_bytes());
75        let encoded = general_purpose::STANDARD.encode(&prefixed);
76
77        let start = (i * 4 + 2) / 3;
78        let end = ((i + n) * 4) / 3;
79
80        if start < end {
81            variants.push(encoded[start..end].to_string());
82        }
83    }
84    variants
85}
86
87/// Sigma-to-secir compiler.
88///
89/// Parses Sigma YAML rules and produces secir [`secir::Template`] instances
90/// suitable for compilation into the Calyx match database.
91#[derive(Debug, Clone, Default)]
92pub struct SigmaCompiler;
93
94impl SigmaCompiler {
95    /// Creates a new compiler instance.
96    #[must_use]
97    pub fn new() -> Self {
98        Self
99    }
100
101    /// Compiles a Sigma YAML string into a secir template.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the YAML cannot be parsed or the rule is invalid.
106    pub fn compile(&self, yaml: &str) -> secir::Result<secir::Template> {
107        let rule: SigmaRule =
108            serde_yaml::from_str(yaml).map_err(|source| secir::Error::TemplateValidation {
109                id: "sigma".to_string(),
110                reason: format!("failed to parse sigma YAML: {source}"),
111            })?;
112        compile_rule(rule)
113    }
114
115    /// Compiles a Sigma YAML string into a secir template (static convenience method).
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the YAML cannot be parsed or the rule is invalid.
120    pub fn compile_str(yaml: &str) -> secir::Result<secir::Template> {
121        Self::new().compile(yaml)
122    }
123}