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}