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}