wazuh_client/
rules.rs

1use reqwest::Method;
2use serde::de::{self, MapAccess, SeqAccess, Visitor}; // Added MapAccess
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::Value;
5use std::fmt;
6use std::marker::PhantomData;
7use tracing::{debug, info};
8
9use super::error::WazuhApiError;
10use super::wazuh_client::WazuhApiClient;
11
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct Rule {
14    pub id: u32,
15    pub level: u32,
16    pub description: String,
17    pub filename: String,
18    pub relative_dirname: String,
19    pub status: String,
20    pub details: Option<RuleDetails>,
21    #[serde(
22        default,
23        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
24    )]
25    pub gdpr: Option<Vec<String>>,
26    #[serde(
27        default,
28        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
29    )]
30    pub gpg13: Option<Vec<String>>,
31    #[serde(
32        default,
33        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
34    )]
35    pub hipaa: Option<Vec<String>>,
36    #[serde(
37        default,
38        rename = "nist-800-53",
39        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
40    )]
41    pub nist_800_53: Option<Vec<String>>,
42    #[serde(
43        default,
44        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
45    )]
46    pub tsc: Option<Vec<String>>,
47    #[serde(
48        default,
49        rename = "pci_dss",
50        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
51    )]
52    pub pci_dss: Option<Vec<String>>,
53    #[serde(
54        default,
55        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
56    )]
57    pub mitre: Option<Vec<String>>,
58    // It needs a deserializer that can turn an empty map {} into an empty Vec.
59    #[serde(deserialize_with = "deserialize_vec_or_empty_map_as_vec")]
60    pub groups: Vec<String>,
61}
62
63fn deserialize_vec_or_empty_map_as_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
64where
65    D: Deserializer<'de>,
66{
67    struct VecOrEmptyMapToVecVisitor(PhantomData<Vec<String>>);
68
69    impl<'de> Visitor<'de> for VecOrEmptyMapToVecVisitor {
70        type Value = Vec<String>;
71
72        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
73            formatter.write_str("a list of strings or an empty map")
74        }
75
76        fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
77        where
78            S: SeqAccess<'de>,
79        {
80            let mut vec = Vec::new();
81            while let Some(element) = seq.next_element()? {
82                vec.push(element);
83            }
84            Ok(vec)
85        }
86
87        fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
88        where
89            M: MapAccess<'de>,
90        {
91            // If it's a map, check if it's empty. If so, treat as an empty Vec.
92            // Otherwise, it's an error as a non-empty map cannot be Vec<String>.
93            if map.next_key::<String>()?.is_none() {
94                Ok(Vec::new()) // Empty map `{}` becomes `vec![]`
95            } else {
96                Err(de::Error::invalid_type(de::Unexpected::Map, &self))
97            }
98        }
99    }
100
101    deserializer.deserialize_any(VecOrEmptyMapToVecVisitor(PhantomData))
102}
103
104// Helper function to deserialize Option<Vec<String>> that might also be an empty map {}
105fn deserialize_vec_or_empty_map_as_option_vec<'de, D>(
106    deserializer: D,
107) -> Result<Option<Vec<String>>, D::Error>
108where
109    D: Deserializer<'de>,
110{
111    struct VecOrEmptyMapVisitor(PhantomData<Option<Vec<String>>>);
112
113    impl<'de> Visitor<'de> for VecOrEmptyMapVisitor {
114        type Value = Option<Vec<String>>;
115
116        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
117            formatter.write_str("a list of strings, null, or an empty map")
118        }
119
120        fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
121        where
122            S: SeqAccess<'de>,
123        {
124            let mut vec = Vec::new();
125            while let Some(element) = seq.next_element()? {
126                vec.push(element);
127            }
128            Ok(Some(vec)) // Handles `[]` as `Some([])`
129        }
130
131        fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
132        where
133            M: MapAccess<'de>,
134        {
135            // Iterate through the map to consume all its entries.
136            // This makes the deserializer treat any map (empty or non-empty) as None.
137            // This is a lenient approach. If a non-empty map contains valuable data
138            // that should be parsed, this approach would discard it.
139            // However, it prevents deserialization errors if the API unexpectedly sends a map.
140            while let Some(_key) = map.next_key::<serde_json::Value>()? {
141                // Consume the value associated with the key
142                let _ = map.next_value::<serde_json::Value>()?;
143            }
144            Ok(None) // Treat any map (empty or non-empty) as None
145        }
146
147        fn visit_none<E>(self) -> Result<Self::Value, E>
148        where
149            E: de::Error,
150        {
151            Ok(None) // Handles `null` as `None`
152        }
153
154        fn visit_unit<E>(self) -> Result<Self::Value, E>
155        where
156            E: de::Error,
157        {
158            Ok(None) // Handles omitted field if #[serde(default)] is used
159        }
160    }
161
162    deserializer.deserialize_any(VecOrEmptyMapVisitor(PhantomData))
163}
164
165#[derive(Debug, Clone, Deserialize, Serialize)]
166pub struct PatternDetail {
167    pub pattern: String,
168}
169
170#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct InfoDetail {
172    pub name: String,
173    #[serde(rename = "type")]
174    pub type_info: String,
175}
176
177fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
178where
179    D: Deserializer<'de>,
180{
181    struct StringOrVec(PhantomData<Vec<String>>);
182
183    impl<'de> Visitor<'de> for StringOrVec {
184        type Value = Option<Vec<String>>;
185
186        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
187            formatter.write_str("string or list of strings")
188        }
189
190        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
191        where
192            E: de::Error,
193        {
194            if value.is_empty() {
195                Ok(None) // Treat empty string as None, or Some(vec![]) if preferred
196            } else {
197                Ok(Some(vec![value.to_owned()]))
198            }
199        }
200
201        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
202        where
203            E: de::Error,
204        {
205            if value.is_empty() {
206                Ok(None)
207            } else {
208                Ok(Some(vec![value]))
209            }
210        }
211
212        fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
213        where
214            S: SeqAccess<'de>,
215        {
216            let mut vec = Vec::new();
217            while let Some(element) = seq.next_element()? {
218                vec.push(element);
219            }
220            if vec.is_empty() {
221                Ok(None)
222            } else {
223                Ok(Some(vec))
224            }
225        }
226
227        fn visit_none<E>(self) -> Result<Self::Value, E>
228        where
229            E: de::Error,
230        {
231            Ok(None)
232        }
233
234        // Handles cases like `[]` if the API sends an empty array for an optional field.
235        fn visit_unit<E>(self) -> Result<Self::Value, E>
236        where
237            E: de::Error,
238        {
239            Ok(None)
240        }
241    }
242
243    deserializer.deserialize_any(StringOrVec(PhantomData))
244}
245
246fn deserialize_empty_string_as_none_bool<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
247where
248    D: Deserializer<'de>,
249{
250    struct EmptyStringAsNoneBool(PhantomData<Option<bool>>);
251
252    impl<'de> Visitor<'de> for EmptyStringAsNoneBool {
253        type Value = Option<bool>;
254
255        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
256            formatter.write_str("a boolean or an empty string")
257        }
258
259        fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
260        where
261            E: de::Error,
262        {
263            Ok(Some(value))
264        }
265
266        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
267        where
268            E: de::Error,
269        {
270            if value.is_empty() {
271                Ok(None)
272            } else {
273                // Attempt to parse string "true" or "false" if API might send that
274                match value.to_lowercase().as_str() {
275                    "true" => Ok(Some(true)),
276                    "false" => Ok(Some(false)),
277                    _ => Err(de::Error::invalid_value(de::Unexpected::Str(value), &self)),
278                }
279            }
280        }
281
282        fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
283        where
284            E: de::Error,
285        {
286            if value.is_empty() {
287                Ok(None)
288            } else {
289                match value.to_lowercase().as_str() {
290                    "true" => Ok(Some(true)),
291                    "false" => Ok(Some(false)),
292                    _ => Err(de::Error::invalid_value(de::Unexpected::Str(&value), &self)),
293                }
294            }
295        }
296
297        fn visit_none<E>(self) -> Result<Self::Value, E>
298        where
299            E: de::Error,
300        {
301            Ok(None)
302        }
303
304        fn visit_unit<E>(self) -> Result<Self::Value, E>
305        where
306            E: de::Error,
307        {
308            Ok(None) // Also treat unit as None, e.g. if API sends `null`
309        }
310    }
311
312    deserializer.deserialize_any(EmptyStringAsNoneBool(PhantomData))
313}
314
315#[derive(Debug, Clone, Deserialize, Serialize)]
316pub struct RuleDetails {
317    pub category: Option<String>,
318    pub if_sid: Option<String>,
319    #[serde(default, deserialize_with = "deserialize_string_or_vec")]
320    pub if_group: Option<Vec<String>>,
321    #[serde(rename = "match")]
322    pub match_obj: Option<PatternDetail>,
323    #[serde(rename = "regex")]
324    pub regex_obj: Option<PatternDetail>,
325    pub order: Option<String>,
326    pub frequency: Option<String>,
327    pub timeframe: Option<String>,
328    pub ignore: Option<String>,
329    #[serde(default, deserialize_with = "deserialize_empty_string_as_none_bool")]
330    pub check_diff: Option<bool>,
331    #[serde(
332        default,
333        deserialize_with = "deserialize_vec_or_empty_map_as_option_vec"
334    )]
335    pub group: Option<Vec<String>>,
336    pub info: Option<Value>,
337    #[serde(default, deserialize_with = "deserialize_string_or_vec")]
338    pub options: Option<Vec<String>>,
339    #[serde(rename = "level")]
340    pub level_detail: Option<PatternDetail>,
341    pub alert_type: Option<PatternDetail>,
342    pub fim_db_table: Option<PatternDetail>,
343    pub status: Option<PatternDetail>,
344    pub action: Option<PatternDetail>,
345    #[serde(rename = "id")]
346    pub id_detail: Option<PatternDetail>,
347    #[serde(rename = "cisco.severity")]
348    pub cisco_severity_detail: Option<PatternDetail>,
349    #[serde(rename = "win.system.severityValue")]
350    pub win_system_severity_value_detail: Option<PatternDetail>,
351    #[serde(rename = "win.eventdata.targetSid")]
352    pub win_eventdata_target_sid_detail: Option<PatternDetail>,
353    #[serde(rename = "win.eventdata.failureCode")]
354    pub win_eventdata_failure_code_detail: Option<PatternDetail>,
355    #[serde(rename = "win.eventdata.image")]
356    pub win_eventdata_image_detail: Option<PatternDetail>,
357    #[serde(rename = "win.eventdata.parentImage")]
358    pub win_eventdata_parent_image_detail: Option<PatternDetail>,
359    #[serde(rename = "win.eventdata.originalFileName")]
360    pub win_eventdata_original_filename_detail: Option<PatternDetail>,
361    #[serde(rename = "win.eventdata.commandLine")]
362    pub win_eventdata_commandline_detail: Option<PatternDetail>,
363    #[serde(rename = "win.eventdata.type")]
364    pub win_eventdata_type_detail: Option<PatternDetail>,
365    #[serde(rename = "win.eventdata.destination")]
366    pub win_eventdata_destination_detail: Option<PatternDetail>,
367    #[serde(rename = "win.system.message")]
368    pub win_system_message_detail: Option<PatternDetail>,
369    #[serde(rename = "win.eventdata.scriptBlockText")]
370    pub win_eventdata_scriptblocktext_detail: Option<PatternDetail>,
371    #[serde(rename = "Severity")]
372    pub severity_detail: Option<PatternDetail>,
373    pub appcat: Option<PatternDetail>,
374    pub pri: Option<PatternDetail>,
375    #[serde(rename = "audit.type")]
376    pub audit_type_detail: Option<PatternDetail>,
377    #[serde(rename = "audit.res")]
378    pub audit_res_detail: Option<PatternDetail>,
379    #[serde(rename = "event.code")]
380    pub event_code_detail: Option<PatternDetail>,
381    #[serde(rename = "cs4Label")]
382    pub cs4label_detail: Option<PatternDetail>,
383    #[serde(rename = "cs4")]
384    pub cs4_detail: Option<PatternDetail>,
385    #[serde(rename = "cn3Label")]
386    pub cn3label_detail: Option<PatternDetail>,
387    #[serde(rename = "cn3")]
388    pub cn3_detail: Option<PatternDetail>,
389    #[serde(rename = "office365.RecordType")]
390    pub office365_recordtype_detail: Option<PatternDetail>,
391    #[serde(rename = "office365.Operation")]
392    pub office365_operation_detail: Option<PatternDetail>,
393    #[serde(rename = "office365.Parameters")]
394    pub office365_parameters_detail: Option<PatternDetail>,
395    #[serde(rename = "github.action")]
396    pub github_action_detail: Option<PatternDetail>,
397    #[serde(rename = "vuls.score")]
398    pub vuls_score_detail: Option<PatternDetail>,
399    #[serde(rename = "vulnerability.status")]
400    pub vulnerability_status_detail: Option<PatternDetail>,
401    #[serde(rename = "vulnerability.severity")]
402    pub vulnerability_severity_detail: Option<PatternDetail>,
403    #[serde(rename = "qualysguard.severity")]
404    pub qualysguard_severity_detail: Option<PatternDetail>,
405    #[serde(rename = "virustotal.malicious")]
406    pub virustotal_malicious_detail: Option<PatternDetail>,
407}
408
409#[derive(Debug, Clone, Deserialize, Serialize)]
410pub struct Decoder {
411    pub name: String,
412    pub filename: String,
413    pub relative_dirname: String,
414    pub status: String,
415    pub position: Option<u32>,
416    pub details: Option<DecoderDetails>,
417}
418
419#[derive(Debug, Clone, Deserialize, Serialize)]
420pub struct DecoderPatternDetail {
421    pub pattern: String,
422    pub offset: Option<String>,
423    #[serde(rename = "type")]
424    pub type_info: Option<String>,
425}
426
427#[derive(Debug, Clone, Deserialize, Serialize)]
428pub struct DecoderDetails {
429    pub program_name: Option<String>,
430    pub order: Option<String>,
431    pub prematch: Option<DecoderPatternDetail>,
432    pub regex: Option<DecoderPatternDetail>,
433    pub parent: Option<String>,
434    pub use_own_name: Option<bool>,
435    pub json_null_field: Option<String>,
436    pub plugin_decoder: Option<String>,
437}
438
439#[derive(Debug, Clone)]
440pub struct RulesClient {
441    api_client: WazuhApiClient,
442}
443
444impl RulesClient {
445    pub fn new(api_client: WazuhApiClient) -> Self {
446        Self { api_client }
447    }
448
449    pub async fn get_rules(
450        &mut self,
451        limit: Option<u32>,
452        offset: Option<u32>,
453        level: Option<u32>,
454        group: Option<&str>,
455        filename: Option<&str>,
456    ) -> Result<Vec<Rule>, WazuhApiError> {
457        debug!("Getting rules list");
458
459        let mut query_params = Vec::new();
460
461        if let Some(limit) = limit {
462            query_params.push(("limit", limit.to_string()));
463        }
464        if let Some(offset) = offset {
465            query_params.push(("offset", offset.to_string()));
466        }
467        if let Some(level) = level {
468            query_params.push(("level", level.to_string()));
469        }
470        if let Some(group) = group {
471            query_params.push(("group", group.to_string()));
472        }
473        if let Some(filename) = filename {
474            query_params.push(("filename", filename.to_string()));
475        }
476
477        let query_params_ref: Vec<(&str, &str)> =
478            query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
479
480        let response = self
481            .api_client
482            .make_request(
483                Method::GET,
484                "/rules",
485                None,
486                if query_params_ref.is_empty() {
487                    None
488                } else {
489                    Some(&query_params_ref)
490                },
491            )
492            .await?;
493
494        let rules_data = response
495            .get("data")
496            .and_then(|d| d.get("affected_items"))
497            .ok_or_else(|| {
498                WazuhApiError::ApiError(
499                    "Missing 'data.affected_items' in rules response".to_string(),
500                )
501            })?;
502
503        let rules: Vec<Rule> = serde_json::from_value(rules_data.clone())?;
504        info!("Retrieved {} rules", rules.len());
505        Ok(rules)
506    }
507
508    pub async fn get_rule(&mut self, rule_id: u32) -> Result<Rule, WazuhApiError> {
509        debug!(%rule_id, "Getting specific rule");
510
511        let rule_id_str = rule_id.to_string();
512        let query_params = [("rule_ids", rule_id_str.as_str())];
513        let response = self
514            .api_client
515            .make_request(Method::GET, "/rules", None, Some(&query_params))
516            .await?;
517
518        let rule_data = response
519            .get("data")
520            .and_then(|d| d.get("affected_items"))
521            .and_then(|items| items.as_array())
522            .and_then(|arr| arr.first())
523            .ok_or_else(|| WazuhApiError::ApiError(format!("Rule {} not found", rule_id)))?;
524
525        let rule: Rule = serde_json::from_value(rule_data.clone())?;
526        info!(%rule_id, "Retrieved rule details");
527        Ok(rule)
528    }
529
530    pub async fn get_rules_by_level(&mut self, level: u32) -> Result<Vec<Rule>, WazuhApiError> {
531        debug!(%level, "Getting rules by level");
532        self.get_rules(None, None, Some(level), None, None).await
533    }
534
535    pub async fn get_rules_by_group(&mut self, group: &str) -> Result<Vec<Rule>, WazuhApiError> {
536        debug!(%group, "Getting rules by group");
537        self.get_rules(None, None, None, Some(group), None).await
538    }
539
540    pub async fn get_high_level_rules(&mut self) -> Result<Vec<Rule>, WazuhApiError> {
541        debug!("Getting high-level rules");
542
543        let query_params = [("level", "10-15")];
544        let response = self
545            .api_client
546            .make_request(Method::GET, "/rules", None, Some(&query_params))
547            .await?;
548
549        let rules_data = response
550            .get("data")
551            .and_then(|d| d.get("affected_items"))
552            .ok_or_else(|| {
553                WazuhApiError::ApiError(
554                    "Missing 'data.affected_items' in high-level rules response".to_string(),
555                )
556            })?;
557
558        let rules: Vec<Rule> = serde_json::from_value(rules_data.clone())?;
559        info!("Retrieved {} high-level rules", rules.len());
560        Ok(rules)
561    }
562
563    pub async fn get_rule_groups(&mut self) -> Result<Vec<String>, WazuhApiError> {
564        debug!("Getting rule groups");
565
566        let response = self
567            .api_client
568            .make_request(Method::GET, "/rules/groups", None, None)
569            .await?;
570
571        let groups_data = response
572            .get("data")
573            .and_then(|d| d.get("affected_items"))
574            .ok_or_else(|| {
575                WazuhApiError::ApiError(
576                    "Missing 'data.affected_items' in rule groups response".to_string(),
577                )
578            })?;
579
580        let groups: Vec<String> = serde_json::from_value(groups_data.clone())?;
581        info!("Retrieved {} rule groups", groups.len());
582        Ok(groups)
583    }
584
585    pub async fn get_decoders(
586        &mut self,
587        limit: Option<u32>,
588        offset: Option<u32>,
589        filename: Option<&str>,
590    ) -> Result<Vec<Decoder>, WazuhApiError> {
591        debug!("Getting decoders list");
592
593        let mut query_params = Vec::new();
594
595        if let Some(limit) = limit {
596            query_params.push(("limit", limit.to_string()));
597        }
598        if let Some(offset) = offset {
599            query_params.push(("offset", offset.to_string()));
600        }
601        if let Some(filename) = filename {
602            query_params.push(("filename", filename.to_string()));
603        }
604
605        let query_params_ref: Vec<(&str, &str)> =
606            query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
607
608        let response = self
609            .api_client
610            .make_request(
611                Method::GET,
612                "/decoders",
613                None,
614                if query_params_ref.is_empty() {
615                    None
616                } else {
617                    Some(&query_params_ref)
618                },
619            )
620            .await?;
621
622        let decoders_data = response
623            .get("data")
624            .and_then(|d| d.get("affected_items"))
625            .ok_or_else(|| {
626                WazuhApiError::ApiError(
627                    "Missing 'data.affected_items' in decoders response".to_string(),
628                )
629            })?;
630
631        let decoders: Vec<Decoder> = serde_json::from_value(decoders_data.clone())?;
632        info!("Retrieved {} decoders", decoders.len());
633        Ok(decoders)
634    }
635
636    pub async fn get_decoder(&mut self, decoder_name: &str) -> Result<Decoder, WazuhApiError> {
637        debug!(%decoder_name, "Getting specific decoder");
638
639        let query_params = [("decoder_names", decoder_name)];
640        let response = self
641            .api_client
642            .make_request(Method::GET, "/decoders", None, Some(&query_params))
643            .await?;
644
645        let decoder_data = response
646            .get("data")
647            .and_then(|d| d.get("affected_items"))
648            .and_then(|items| items.as_array())
649            .and_then(|arr| arr.first())
650            .ok_or_else(|| {
651                WazuhApiError::ApiError(format!("Decoder {} not found", decoder_name))
652            })?;
653
654        let decoder: Decoder = serde_json::from_value(decoder_data.clone())?;
655        info!(%decoder_name, "Retrieved decoder details");
656        Ok(decoder)
657    }
658
659    pub async fn search_rules(&mut self, search_term: &str) -> Result<Vec<Rule>, WazuhApiError> {
660        debug!(%search_term, "Searching rules by description");
661
662        let query_params = [("search", search_term)];
663        let response = self
664            .api_client
665            .make_request(Method::GET, "/rules", None, Some(&query_params))
666            .await?;
667
668        let rules_data = response
669            .get("data")
670            .and_then(|d| d.get("affected_items"))
671            .ok_or_else(|| {
672                WazuhApiError::ApiError(
673                    "Missing 'data.affected_items' in search rules response".to_string(),
674                )
675            })?;
676
677        let rules: Vec<Rule> = serde_json::from_value(rules_data.clone())?;
678        info!(%search_term, "Found {} rules matching search", rules.len());
679        Ok(rules)
680    }
681}