sigma_rust/
rule.rs

1use crate::detection::Detection;
2use crate::event::Event;
3use serde::Deserialize;
4use std::collections::HashMap;
5
6/// Declares the status of the rule
7#[derive(Deserialize, PartialEq, Debug)]
8#[serde(rename_all = "snake_case")]
9pub enum Status {
10    /// the rule is considered as stable and may be used in production systems or dashboards.
11    Stable,
12    /// a mostly stable rule that could require some slight adjustments depending on the environment.
13    Test,
14    /// an experimental rule that could lead to false positives results or be noisy, but could also identify interesting events.
15    Experimental,
16    /// the rule is replaced or covered by another one. The link is established by the related field.
17    Deprecated,
18    /// the rule cannot be used in its current state (old correlation format, custom fields)
19    Unsupported,
20}
21
22/// To be able to keep track of the relationships between detections, Sigma rules may also contain references to related rule identifiers in the related attribute.
23/// This allows to define common relationships between detections as follows:
24/// ```yaml
25/// related:
26///   - id: 08fbc97d-0a2f-491c-ae21-8ffcfd3174e9
27///     type: derived
28///   - id: 929a690e-bef0-4204-a928-ef5e620d6fcc
29///     type: obsolete
30/// ```
31#[derive(Deserialize, Debug)]
32pub struct Related {
33    pub id: String,
34    #[serde(rename = "type")]
35    pub related_type: RelatedType,
36}
37
38/// The related type describes the relationship between the rule and the referred rule.
39#[derive(Deserialize, PartialEq, Debug)]
40#[serde(rename_all = "snake_case")]
41pub enum RelatedType {
42    /// The rule was derived from the referred rule or rules, which may remain active.
43    Derived,
44    /// The rule obsoletes the referred rule or rules, which aren't used anymore.
45    Obsolete,
46    /// The rule was merged from the referred rules. The rules may still exist and are in use.
47    Merged,
48    /// The rule had previously the referred identifier or identifiers but was renamed for whatever reason, e.g. from a private naming scheme to UUIDs, to resolve collisions etc. It's not expected that a rule with this id exists anymore.
49    Renamed,
50    /// Use to relate similar rules to each other (e.g. same detection content applied to different log sources, rule that is a modified version of another rule with a different level)
51    Similar,
52}
53
54/// The logsource describes the log data on which the detection is meant to be applied to.
55/// It describes the log source, the platform, the application and the type that is required in the detection.
56#[derive(Deserialize, Debug)]
57pub struct Logsource {
58    /// The category value is used to select all log files written of a logical group.
59    /// This may cover one or more sources of information depending on the system.
60    /// e.g. "antivirus" for the scan result, "webserver" for the web access logs.
61    pub category: Option<String>,
62    /// The product value is used to select all log outputs of a certain product.
63    /// It can be as generic as an operating system or the name of a particular software package.
64    /// e.g. "windows" will include "Security", "System", "Application" and the other like "AppLocker" and "Windows Defender"...
65    pub product: Option<String>,
66    /// The service value is used to select a more specific subset of logs.
67    /// e.g. "sshd" on Linux or the "Security" Eventlog on Windows systems.
68    pub service: Option<String>,
69    /// The definition can be used to describe the log source, including some information on the log verbosity level or configurations that have to be applied.
70    pub definition: Option<String>,
71}
72
73/// The level describes the criticality of a triggered rule.
74/// While low and medium level events have an informative character,
75/// events with high and critical level should lead to immediate reviews by security analysts.
76#[derive(Deserialize, PartialEq, Debug)]
77#[serde(rename_all = "snake_case")]
78pub enum Level {
79    /// Rule is intended for enrichment of events, e.g. by tagging them. No case or alerting should be triggered by such rules because it is expected that a huge amount of events will match these rules.
80    Informational,
81    /// Notable event but rarely an incident. Low rated events can be relevant in high numbers or combination with others. Immediate reaction shouldn't be necessary, but a regular review is recommended.
82    Low,
83    /// Relevant event that should be reviewed manually on a more frequent basis.
84    Medium,
85    /// Relevant event that should trigger an internal alert and requires a prompt review.
86    High,
87    /// Highly relevant event that indicates an incident. Critical events should be reviewed immediately. It is used only for cases in which probability borders certainty.
88    Critical,
89}
90
91/// The `Rule` struct implements the Sigma rule specification 2.0.0 released 08.08.2024.
92///
93/// The full specification can be found at:
94/// <https://github.com/SigmaHQ/sigma-specification/blob/main/specification/sigma-rules-specification.md>
95#[derive(Deserialize, Debug)]
96pub struct Rule {
97    /// A brief title for the rule that should contain what the rule is supposed to detect (max. 256 characters)
98    pub title: String,
99    /// Sigma rules should be identified by a globally unique identifier in the id attribute.
100    /// For this purpose randomly generated UUIDs (version 4) is used.
101    pub id: Option<String>,
102    /// name is a unique human-readable name that can be used instead of the id as a reference in correlation rules.
103    /// The goal is to improve the readability of correlation rules.
104    pub name: Option<String>,
105    /// To be able to keep track of the relationships between detections, Sigma rules may also contain references to related rule identifiers in the related attribute.
106    pub related: Option<Vec<Related>>,
107    pub taxonomy: Option<String>,
108    pub status: Option<Status>,
109    /// A short and accurate description of the rule and the malicious or suspicious activity that can be detected (max. 65,535 characters)
110    pub description: Option<String>,
111    /// License of the rule according to <https://spdx.dev/learn/handling-license-info/> format.
112    pub license: Option<String>,
113    /// Creator of the rule. (can be a name, nickname, twitter handle...etc)
114    /// If there is more than one, they are separated by a comma.
115    pub author: Option<String>,
116    /// References to the sources that the rule was derived from.
117    /// These could be blog articles, technical papers, presentations or even tweets.
118    pub references: Option<Vec<String>>,
119    /// Creation date of the rule.
120    /// Use the ISO 8601 date with separator format : YYYY-MM-DD
121    pub date: Option<String>,
122    /// Last modification date of the rule.
123    /// Use the ISO 8601 date with separator format : YYYY-MM-DD
124    pub modified: Option<String>,
125    /// This section describes the log data on which the detection is meant to be applied to.
126    /// It describes the log source, the platform, the application and the type that is required in the detection.
127    pub logsource: Logsource,
128    /// A set of search-identifiers that represent properties of searches on log data.
129    pub detection: Detection,
130    /// A list of log fields that could be interesting for further analysis of the event
131    /// and should be displayed to the analyst.
132    pub fields: Option<Vec<String>>,
133    /// A list of known false positives that may occur.
134    pub falsepositives: Option<Vec<String>>,
135    /// The level field contains one of five string values.
136    /// It describes the criticality of a triggered rule.
137    /// While low and medium level events have an informative character,
138    /// events with high and critical level should lead to immediate reviews by security analysts.
139    pub level: Option<Level>,
140    ///  Tags should generally follow this syntax:
141    /// * Character set: lower-case letters, numerals, underscores and hyphens
142    /// * no spaces
143    /// * Tags are namespaced, the dot is used as separator. e.g. attack.t1234 refers to technique 1234 in the namespace attack; Namespaces may also be nested
144    /// * Keep tags short, e.g. numeric identifiers instead of long sentences
145    pub tags: Option<Vec<String>>,
146    /// Capture any additional fields
147    #[serde(flatten)]
148    pub custom_fields: HashMap<String, serde_yml::Value>,
149}
150
151impl Rule {
152    /// Check if the event matches the rule
153    ///
154    /// # Example
155    /// ```rust
156    /// use sigma_rust::{rule_from_yaml, Event, Rule};
157    /// let rule_yaml = r#"
158    /// title: Some test title
159    /// logsource:
160    ///     category: test
161    /// detection:
162    ///     selection_1:
163    ///         field_name|contains:
164    ///             - this
165    ///             - that
166    ///     selection_2:
167    ///         null_field: null
168    ///     condition: all of selection_*
169    /// "#;
170    /// let rule = rule_from_yaml(rule_yaml).unwrap();
171    /// let mut event = Event::from([("field_name", "this")]);
172    /// event.insert("null_field", None);
173    /// assert!(rule.is_match(&event));
174    /// ```
175    pub fn is_match(&self, event: &Event) -> bool {
176        self.detection.evaluate(event)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::field::FieldValue;
184    use crate::selection::Selection;
185    use crate::wildcard::WildcardToken;
186
187    #[test]
188    fn test_load_from_yaml() {
189        let rule_yaml = r#"
190        title: Some test title
191        id: fb97a1c5-9e86-4e15-9fd9-7d82a05a384e
192        name: a unique name
193        related:
194            - id: ab97a1c5-9e86-4e15-9fd9-7d82a05a384e
195              type: derived
196            - id: bb97a1c5-9e86-4e15-9fd9-7d82a05a384e
197              type: obsolete
198        status: stable
199        license: MIT
200        author: Chuck Norris
201        date: 2020-12-30
202        logsource:
203            category: process_creation
204            product: windows
205        level: medium
206        detection:
207          selection:
208            field_name:
209              - this # or
210              - that
211          condition: selection
212        custom_field: some value
213        another_custom_field:
214            nested: nested_value
215        "#;
216        let rule: Rule = serde_yml::from_str(rule_yaml).unwrap();
217        assert_eq!(rule.title, "Some test title");
218        assert_eq!(
219            rule.id,
220            Some("fb97a1c5-9e86-4e15-9fd9-7d82a05a384e".to_string())
221        );
222        assert_eq!(rule.name, Some("a unique name".to_string()));
223        let related = rule.related.as_ref().unwrap();
224        assert_eq!(related.len(), 2);
225        assert_eq!(related[0].id, "ab97a1c5-9e86-4e15-9fd9-7d82a05a384e");
226        assert_eq!(related[0].related_type, RelatedType::Derived);
227        assert_eq!(related[1].id, "bb97a1c5-9e86-4e15-9fd9-7d82a05a384e");
228        assert_eq!(related[1].related_type, RelatedType::Obsolete);
229        assert!(rule.taxonomy.is_none());
230        assert_eq!(rule.status, Some(Status::Stable));
231        assert!(rule.description.is_none());
232        assert_eq!(rule.license, Some("MIT".to_string()));
233        assert_eq!(rule.author, Some("Chuck Norris".to_string()));
234        assert!(rule.references.is_none());
235        assert_eq!(rule.date, Some("2020-12-30".to_string()));
236        assert!(rule.modified.is_none());
237        assert_eq!(
238            rule.logsource.category.as_ref().unwrap(),
239            "process_creation"
240        );
241        assert_eq!(rule.logsource.product.as_ref().unwrap(), "windows");
242        assert!(rule.logsource.service.is_none());
243        assert!(rule.logsource.definition.is_none());
244        assert!(rule.fields.is_none());
245        assert!(rule.falsepositives.is_none());
246        assert_eq!(rule.level.as_ref().unwrap(), &Level::Medium);
247        assert!(rule.tags.is_none());
248        assert_eq!(rule.detection.get_selections().len(), 1);
249        match rule.detection.get_selections().get("selection").unwrap() {
250            Selection::Keyword(_) => panic!("Wrong selection type"),
251            Selection::Field(field_groups) => {
252                assert_eq!(field_groups.len(), 1);
253                let fields = &field_groups[0].fields;
254                assert_eq!(fields.len(), 1);
255                assert_eq!(fields[0].name, "field_name");
256                assert_eq!(fields[0].values.len(), 2);
257
258                assert!(
259                    matches!(&fields[0].values[0], FieldValue::WildcardPattern(pattern) if pattern.len() == 1 && matches!(&pattern[0], WildcardToken::Pattern(p) if p == &"this".chars().collect::<Vec<char>>()))
260                );
261
262                assert!(
263                    matches!(&fields[0].values[1], FieldValue::WildcardPattern(pattern) if pattern.len() == 1 && matches!(&pattern[0], WildcardToken::Pattern(p) if p == &"that".chars().collect::<Vec<char>>()))
264                );
265            }
266        }
267
268        assert_eq!(rule.detection.get_condition(), "selection".to_string());
269        assert_eq!(rule.custom_fields["custom_field"], "some value");
270        assert_eq!(
271            rule.custom_fields["another_custom_field"]["nested"],
272            "nested_value"
273        );
274
275        let event = Event::from([("field_name", "this")]);
276        assert!(rule.is_match(&event));
277    }
278}