rsigma_runtime/risk/
object.rs1use rsigma_eval::EvaluationResult;
10use serde::Serialize;
11
12use crate::selector::Selector;
13
14#[derive(Debug, Clone)]
16pub struct ObjectSelector {
17 pub object_type: String,
19 pub selector: Selector,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25pub struct RiskObject {
26 #[serde(rename = "type")]
28 pub object_type: String,
29 pub value: String,
31}
32
33pub fn extract(result: &EvaluationResult, selectors: &[ObjectSelector]) -> Vec<RiskObject> {
37 let mut out: Vec<RiskObject> = Vec::new();
38 for sel in selectors {
39 let Some(value) = sel.selector.resolve(result) else {
40 continue;
41 };
42 let Some(value) = scalar_to_string(&value) else {
43 continue;
44 };
45 let object = RiskObject {
46 object_type: sel.object_type.clone(),
47 value,
48 };
49 if !out.contains(&object) {
50 out.push(object);
51 }
52 }
53 out
54}
55
56fn scalar_to_string(value: &serde_json::Value) -> Option<String> {
59 match value {
60 serde_json::Value::String(s) if !s.is_empty() => Some(s.clone()),
61 serde_json::Value::String(_) => None,
62 serde_json::Value::Number(n) => Some(n.to_string()),
63 serde_json::Value::Bool(b) => Some(b.to_string()),
64 serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
65 None
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use rsigma_eval::{DetectionBody, FieldMatch, ResultBody, RuleHeader};
74 use rsigma_parser::Level;
75 use std::collections::HashMap;
76 use std::sync::Arc;
77
78 fn detection() -> EvaluationResult {
79 EvaluationResult {
80 header: RuleHeader {
81 rule_title: "t".to_string(),
82 rule_id: Some("r".to_string()),
83 level: Some(Level::High),
84 tags: vec![],
85 custom_attributes: Arc::new(HashMap::new()),
86 enrichments: Some(
87 serde_json::json!({"user": "alice"})
88 .as_object()
89 .unwrap()
90 .clone(),
91 ),
92 },
93 body: ResultBody::Detection(DetectionBody {
94 matched_selections: vec![],
95 matched_fields: vec![
96 FieldMatch::new("SourceIp", serde_json::json!("10.0.0.1")),
97 FieldMatch::new("SourceIp", serde_json::json!("10.0.0.1")),
98 ],
99 event: Some(serde_json::json!({"host": {"name": "dc01"}})),
100 }),
101 }
102 }
103
104 fn sel(object_type: &str, raw: &str) -> ObjectSelector {
105 ObjectSelector {
106 object_type: object_type.to_string(),
107 selector: Selector::parse(raw).unwrap(),
108 }
109 }
110
111 #[test]
112 fn extracts_across_namespaces() {
113 let objects = extract(
114 &detection(),
115 &[
116 sel("src_ip", "match.SourceIp"),
117 sel("host", "event.host.name"),
118 sel("user", "enrichment.user"),
119 ],
120 );
121 assert_eq!(
122 objects,
123 vec![
124 RiskObject {
125 object_type: "src_ip".into(),
126 value: "10.0.0.1".into()
127 },
128 RiskObject {
129 object_type: "host".into(),
130 value: "dc01".into()
131 },
132 RiskObject {
133 object_type: "user".into(),
134 value: "alice".into()
135 },
136 ]
137 );
138 }
139
140 #[test]
141 fn missing_selector_contributes_nothing() {
142 let objects = extract(&detection(), &[sel("user", "event.nope")]);
143 assert!(objects.is_empty());
144 }
145
146 #[test]
147 fn duplicate_objects_are_collapsed() {
148 let objects = extract(
149 &detection(),
150 &[
151 sel("src_ip", "match.SourceIp"),
152 sel("src_ip", "match.SourceIp"),
153 ],
154 );
155 assert_eq!(objects.len(), 1);
156 }
157}