1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::fmt;
5use tracing::{debug, info, warn};
6
7use super::indexer_client::WazuhIndexerClient;
8use super::wazuh_client::WazuhApiClient;
9
10mod string_or_number_as_string {
11 use serde::{Deserialize, Deserializer};
12
13 pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
14 where
15 D: Deserializer<'de>,
16 {
17 #[derive(Deserialize)]
18 #[serde(untagged)]
19 enum StringOrNumber {
20 Str(String),
21 Num(serde_json::Number),
22 }
23
24 match StringOrNumber::deserialize(deserializer)? {
25 StringOrNumber::Str(s) => Ok(s),
26 StringOrNumber::Num(n) => Ok(n.to_string()),
27 }
28 }
29
30 pub fn deserialize_optional<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
31 where
32 D: Deserializer<'de>,
33 {
34 #[derive(Deserialize)]
35 #[serde(untagged)]
36 enum StringOrNumberOrNull {
37 Str(String),
38 Num(serde_json::Number),
39 }
40
41 let intermediate: Option<StringOrNumberOrNull> = Option::deserialize(deserializer)?;
42
43 match intermediate {
44 Some(StringOrNumberOrNull::Str(s)) => Ok(Some(s)),
45 Some(StringOrNumberOrNull::Num(n)) => Ok(Some(n.to_string())),
46 None => Ok(None),
47 }
48 }
49}
50use super::error::WazuhApiError;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(rename_all = "PascalCase")]
54pub enum VulnerabilitySeverity {
55 Critical,
56 High,
57 Medium,
58 Low,
59}
60
61impl VulnerabilitySeverity {
62 pub fn to_indexer_format(&self) -> &'static str {
64 match self {
65 VulnerabilitySeverity::Critical => "Critical",
66 VulnerabilitySeverity::High => "High",
67 VulnerabilitySeverity::Medium => "Medium",
68 VulnerabilitySeverity::Low => "Low",
69 }
70 }
71
72 pub fn parse(s: &str) -> Option<Self> {
74 match s.to_lowercase().as_str() {
75 "critical" => Some(VulnerabilitySeverity::Critical),
76 "high" => Some(VulnerabilitySeverity::High),
77 "medium" => Some(VulnerabilitySeverity::Medium),
78 "low" => Some(VulnerabilitySeverity::Low),
79 _ => None,
80 }
81 }
82
83 pub fn all() -> [VulnerabilitySeverity; 4] {
85 [
86 VulnerabilitySeverity::Critical,
87 VulnerabilitySeverity::High,
88 VulnerabilitySeverity::Medium,
89 VulnerabilitySeverity::Low,
90 ]
91 }
92}
93
94impl fmt::Display for VulnerabilitySeverity {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(f, "{}", self.to_indexer_format())
97 }
98}
99
100impl From<VulnerabilitySeverity> for String {
101 fn from(severity: VulnerabilitySeverity) -> Self {
102 severity.to_indexer_format().to_string()
103 }
104}
105
106#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct Vulnerability {
108 pub cve: String,
109 pub title: String,
110 #[serde(deserialize_with = "deserialize_severity")]
111 pub severity: VulnerabilitySeverity,
112 pub published: Option<String>,
113 pub updated: Option<String>,
114 pub reference: Option<String>,
115 pub description: Option<String>,
116 pub cvss: Option<CvssScore>,
117 pub detection_time: Option<String>,
118 pub agent_id: Option<String>,
119 pub agent_name: Option<String>,
120}
121
122fn deserialize_severity<'de, D>(deserializer: D) -> Result<VulnerabilitySeverity, D::Error>
123where
124 D: serde::Deserializer<'de>,
125{
126 let s = String::deserialize(deserializer)?;
127 VulnerabilitySeverity::parse(&s)
128 .ok_or_else(|| serde::de::Error::custom(format!("Invalid severity: {}", s)))
129}
130
131#[derive(Debug, Clone, Deserialize, Serialize)]
132pub struct CvssScore {
133 pub cvss2: Option<CvssDetails>,
134 pub cvss3: Option<CvssDetails>,
135}
136
137#[derive(Debug, Clone, Deserialize, Serialize)]
138pub struct CvssDetails {
139 pub vector: Option<String>,
140 pub base_score: Option<f64>,
141}
142
143#[derive(Debug, Clone, Deserialize, Serialize)]
144pub struct Package {
145 pub name: String,
146 pub version: String,
147 pub architecture: Option<String>,
148 pub format: Option<String>,
149 pub description: Option<String>,
150 pub size: Option<u64>,
151 pub vendor: Option<String>,
152 pub multiarch: Option<String>,
153 pub source: Option<String>,
154 pub priority: Option<String>,
155 pub scan_id: Option<u64>,
156 pub section: Option<String>,
157 pub agent_id: Option<String>,
158}
159
160#[derive(Debug, Clone, Deserialize, Serialize)]
161pub struct Process {
162 #[serde(deserialize_with = "string_or_number_as_string::deserialize")]
163 pub pid: String,
164 pub name: String,
165 pub state: Option<String>,
166 #[serde(
167 default,
168 deserialize_with = "string_or_number_as_string::deserialize_optional"
169 )]
170 pub ppid: Option<String>,
171 #[serde(
172 default,
173 deserialize_with = "string_or_number_as_string::deserialize_optional"
174 )]
175 pub utime: Option<String>,
176 #[serde(
177 default,
178 deserialize_with = "string_or_number_as_string::deserialize_optional"
179 )]
180 pub stime: Option<String>,
181 pub cmd: Option<String>,
182 pub argvs: Option<String>,
183 pub euser: Option<String>,
184 pub ruser: Option<String>,
185 pub suser: Option<String>,
186 pub egroup: Option<String>,
187 pub rgroup: Option<String>,
188 pub sgroup: Option<String>,
189 pub fgroup: Option<String>,
190 pub priority: Option<i32>,
191 pub nice: Option<i32>,
192 pub size: Option<u64>,
193 pub vm_size: Option<u64>,
194 pub resident: Option<u64>,
195 pub share: Option<u64>,
196 #[serde(
197 default,
198 deserialize_with = "string_or_number_as_string::deserialize_optional"
199 )]
200 pub start_time: Option<String>,
201 #[serde(
202 default,
203 deserialize_with = "string_or_number_as_string::deserialize_optional"
204 )]
205 pub pgrp: Option<String>,
206 #[serde(
207 default,
208 deserialize_with = "string_or_number_as_string::deserialize_optional"
209 )]
210 pub session: Option<String>,
211 pub nlwp: Option<u32>,
212 #[serde(
213 default,
214 deserialize_with = "string_or_number_as_string::deserialize_optional"
215 )]
216 pub tgid: Option<String>,
217 #[serde(
218 default,
219 deserialize_with = "string_or_number_as_string::deserialize_optional"
220 )]
221 pub tty: Option<String>,
222 #[serde(
223 default,
224 deserialize_with = "string_or_number_as_string::deserialize_optional"
225 )]
226 pub processor: Option<String>,
227 pub scan_id: Option<u64>,
228 pub agent_id: Option<String>,
229}
230
231#[derive(Debug, Clone, Deserialize, Serialize)]
232pub struct Port {
233 pub local: PortInfo,
234 pub remote: Option<PortInfo>,
235 pub state: Option<String>,
236 pub protocol: String,
237 pub tx_queue: Option<u32>,
238 pub rx_queue: Option<u32>,
239 pub inode: Option<u64>,
240 pub process: Option<String>,
241 pub scan_id: Option<u64>,
242 pub agent_id: Option<String>,
243 pub pid: Option<u32>,
244}
245
246#[derive(Debug, Clone, Deserialize, Serialize)]
247pub struct VulnerabilitySummaryCounts {
248 pub critical: i32,
249 pub high: i32,
250 pub medium: i32,
251 pub low: i32,
252}
253
254#[derive(Debug, Clone, Deserialize, Serialize)]
255pub struct VulnerabilitySummaryResponseData {
256 pub agent_id: String,
257 pub summary: VulnerabilitySummaryCounts,
258}
259
260#[derive(Debug, Clone, Deserialize, Serialize)]
261pub struct PortInfo {
262 pub ip: Option<String>,
263 pub port: u16,
264}
265
266#[derive(Debug, Clone)]
267pub struct VulnerabilityClient {
268 api_client: WazuhApiClient,
269 indexer_client: WazuhIndexerClient,
270}
271
272impl VulnerabilityClient {
273 pub fn new(api_client: WazuhApiClient, indexer_client: WazuhIndexerClient) -> Self {
274 Self {
275 api_client,
276 indexer_client,
277 }
278 }
279
280 pub async fn get_agent_vulnerabilities(
281 &mut self,
282 agent_id: &str,
283 limit: Option<u32>,
284 offset: Option<u32>,
285 severity: Option<VulnerabilitySeverity>,
286 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
287 debug!(%agent_id, ?severity, "Getting vulnerabilities for agent");
288
289 let size = limit.unwrap_or(100);
290 let from = offset.unwrap_or(0);
291
292 let mut must_clauses = vec![json!({
293 "term": {
294 "agent.id": agent_id
295 }
296 })];
297
298 if let Some(severity) = severity {
299 must_clauses.push(json!({
300 "term": {
301 "vulnerability.severity": severity.to_indexer_format()
302 }
303 }));
304 }
305
306 let query_body = json!({
307 "size": size,
308 "from": from,
309 "query": {
310 "bool": {
311 "must": must_clauses
312 }
313 }
314 });
315
316 let endpoint = "/wazuh-states-vulnerabilities*/_search";
317 let response = self
318 .indexer_client
319 .make_indexer_request(Method::POST, endpoint, Some(query_body))
320 .await?;
321
322 let hits = response
323 .get("hits")
324 .and_then(|h| h.get("hits"))
325 .and_then(|h_array| h_array.as_array())
326 .ok_or_else(|| {
327 WazuhApiError::ApiError("Missing 'hits.hits' in vulnerability response".to_string())
328 })?;
329
330 let mut vulnerabilities = Vec::new();
331 for hit in hits {
332 if let Some(source) = hit.get("_source") {
333 if let Ok(vuln) = self.parse_vulnerability_from_source(source, agent_id) {
334 vulnerabilities.push(vuln);
335 }
336 }
337 }
338
339 info!(%agent_id, "Retrieved {} vulnerabilities", vulnerabilities.len());
340 Ok(vulnerabilities)
341 }
342
343 fn parse_vulnerability_from_source(
344 &self,
345 source: &Value,
346 agent_id: &str,
347 ) -> Result<Vulnerability, WazuhApiError> {
348 let vulnerability_data = source.get("vulnerability").ok_or_else(|| {
349 WazuhApiError::ApiError("Missing vulnerability data in source".to_string())
350 })?;
351
352 let cve = vulnerability_data
353 .get("cve")
354 .and_then(|v| v.as_str())
355 .unwrap_or("Unknown")
356 .to_string();
357
358 let title = vulnerability_data
359 .get("title")
360 .and_then(|v| v.as_str())
361 .unwrap_or("No title available")
362 .to_string();
363
364 let severity = vulnerability_data
365 .get("severity")
366 .and_then(|v| v.as_str())
367 .and_then(VulnerabilitySeverity::parse)
368 .unwrap_or(VulnerabilitySeverity::Low); let published = vulnerability_data
371 .get("published")
372 .and_then(|v| v.as_str())
373 .map(|s| s.to_string());
374
375 let updated = vulnerability_data
376 .get("updated")
377 .and_then(|v| v.as_str())
378 .map(|s| s.to_string());
379
380 let reference = vulnerability_data
381 .get("reference")
382 .and_then(|v| v.as_str())
383 .map(|s| s.to_string());
384
385 let description = vulnerability_data
386 .get("description")
387 .and_then(|v| v.as_str())
388 .map(|s| s.to_string());
389
390 let detection_time = source
391 .get("timestamp")
392 .or_else(|| source.get("@timestamp"))
393 .or_else(|| source.get("vulnerability.detection_time"))
394 .and_then(|v| v.as_str())
395 .map(|s| s.to_string());
396
397 let agent_name = source
398 .get("agent")
399 .and_then(|a| a.get("name"))
400 .and_then(|v| v.as_str())
401 .map(|s| s.to_string());
402
403 let cvss = self.parse_cvss_scores(vulnerability_data);
404
405 Ok(Vulnerability {
406 cve,
407 title,
408 severity,
409 published,
410 updated,
411 reference,
412 description,
413 cvss,
414 detection_time,
415 agent_id: Some(agent_id.to_string()),
416 agent_name,
417 })
418 }
419
420 fn parse_cvss_scores(&self, vulnerability_data: &Value) -> Option<CvssScore> {
421 let cvss2 = vulnerability_data
422 .get("cvss2")
423 .map(|cvss2_data| CvssDetails {
424 vector: cvss2_data
425 .get("vector")
426 .and_then(|v| v.as_str())
427 .map(|s| s.to_string()),
428 base_score: cvss2_data.get("base_score").and_then(|v| v.as_f64()),
429 });
430
431 let cvss3 = vulnerability_data
432 .get("cvss3")
433 .map(|cvss3_data| CvssDetails {
434 vector: cvss3_data
435 .get("vector")
436 .and_then(|v| v.as_str())
437 .map(|s| s.to_string()),
438 base_score: cvss3_data.get("base_score").and_then(|v| v.as_f64()),
439 });
440
441 if cvss2.is_some() || cvss3.is_some() {
442 Some(CvssScore { cvss2, cvss3 })
443 } else {
444 None
445 }
446 }
447
448 pub async fn get_critical_vulnerabilities(
449 &mut self,
450 agent_id: &str,
451 limit: Option<u32>,
452 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
453 debug!(%agent_id, "Getting critical vulnerabilities");
454 self.get_agent_vulnerabilities(agent_id, limit, None, Some(VulnerabilitySeverity::Critical))
455 .await
456 }
457
458 pub async fn get_high_vulnerabilities(
459 &mut self,
460 agent_id: &str,
461 limit: Option<u32>,
462 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
463 debug!(%agent_id, "Getting high severity vulnerabilities");
464 self.get_agent_vulnerabilities(agent_id, limit, None, Some(VulnerabilitySeverity::High))
465 .await
466 }
467
468 pub async fn get_medium_vulnerabilities(
469 &mut self,
470 agent_id: &str,
471 limit: Option<u32>,
472 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
473 debug!(%agent_id, "Getting medium severity vulnerabilities");
474 self.get_agent_vulnerabilities(agent_id, limit, None, Some(VulnerabilitySeverity::Medium))
475 .await
476 }
477
478 pub async fn get_low_vulnerabilities(
479 &mut self,
480 agent_id: &str,
481 limit: Option<u32>,
482 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
483 debug!(%agent_id, "Getting low severity vulnerabilities");
484 self.get_agent_vulnerabilities(agent_id, limit, None, Some(VulnerabilitySeverity::Low))
485 .await
486 }
487
488 pub async fn get_agent_packages(
489 &mut self,
490 agent_id: &str,
491 limit: Option<u32>,
492 offset: Option<u32>,
493 search: Option<&str>,
494 ) -> Result<Vec<Package>, WazuhApiError> {
495 debug!(%agent_id, ?search, "Getting packages for agent");
496
497 let mut query_params = Vec::new();
498
499 if let Some(limit) = limit {
500 query_params.push(("limit", limit.to_string()));
501 }
502 if let Some(offset) = offset {
503 query_params.push(("offset", offset.to_string()));
504 }
505 if let Some(search) = search {
506 query_params.push(("search", search.to_string()));
507 }
508
509 let query_params_ref: Vec<(&str, &str)> =
510 query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
511
512 let endpoint = format!("/syscollector/{}/packages", agent_id);
513 let response = self
514 .api_client
515 .make_request(
516 Method::GET,
517 &endpoint,
518 None,
519 if query_params_ref.is_empty() {
520 None
521 } else {
522 Some(&query_params_ref)
523 },
524 )
525 .await?;
526
527 let packages_data = response
528 .get("data")
529 .and_then(|d| d.get("affected_items"))
530 .ok_or_else(|| {
531 WazuhApiError::ApiError(
532 "Missing 'data.affected_items' in packages response".to_string(),
533 )
534 })?;
535
536 let packages: Vec<Package> = serde_json::from_value(packages_data.clone())?;
537 info!(%agent_id, "Retrieved {} packages", packages.len());
538 Ok(packages)
539 }
540
541 pub async fn get_agent_processes(
542 &mut self,
543 agent_id: &str,
544 limit: Option<u32>,
545 offset: Option<u32>,
546 search: Option<&str>,
547 ) -> Result<Vec<Process>, WazuhApiError> {
548 debug!(%agent_id, ?search, "Getting processes for agent");
549
550 let mut query_params = Vec::new();
551
552 if let Some(limit) = limit {
553 query_params.push(("limit", limit.to_string()));
554 }
555 if let Some(offset) = offset {
556 query_params.push(("offset", offset.to_string()));
557 }
558 if let Some(search) = search {
559 query_params.push(("search", search.to_string()));
560 }
561
562 let query_params_ref: Vec<(&str, &str)> =
563 query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
564
565 let endpoint = format!("/syscollector/{}/processes", agent_id);
566 let response = self
567 .api_client
568 .make_request(
569 Method::GET,
570 &endpoint,
571 None,
572 if query_params_ref.is_empty() {
573 None
574 } else {
575 Some(&query_params_ref)
576 },
577 )
578 .await?;
579
580 let processes_data = response
581 .get("data")
582 .and_then(|d| d.get("affected_items"))
583 .ok_or_else(|| {
584 WazuhApiError::ApiError(
585 "Missing 'data.affected_items' in processes response".to_string(),
586 )
587 })?;
588
589 let processes: Vec<Process> = serde_json::from_value(processes_data.clone())?;
590 info!(%agent_id, "Retrieved {} processes", processes.len());
591 Ok(processes)
592 }
593
594 pub async fn get_agent_ports(
595 &mut self,
596 agent_id: &str,
597 limit: Option<u32>,
598 offset: Option<u32>,
599 protocol: Option<&str>,
600 ) -> Result<Vec<Port>, WazuhApiError> {
601 debug!(%agent_id, ?protocol, "Getting ports for agent");
602
603 let mut query_params = Vec::new();
604
605 if let Some(limit) = limit {
606 query_params.push(("limit", limit.to_string()));
607 }
608 if let Some(offset) = offset {
609 query_params.push(("offset", offset.to_string()));
610 }
611 if let Some(protocol) = protocol {
612 query_params.push(("protocol", protocol.to_string()));
613 }
614
615 let query_params_ref: Vec<(&str, &str)> =
616 query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
617
618 let endpoint = format!("/syscollector/{}/ports", agent_id);
619 let response = self
620 .api_client
621 .make_request(
622 Method::GET,
623 &endpoint,
624 None,
625 if query_params_ref.is_empty() {
626 None
627 } else {
628 Some(&query_params_ref)
629 },
630 )
631 .await?;
632
633 let ports_data = response
634 .get("data")
635 .and_then(|d| d.get("affected_items"))
636 .ok_or_else(|| {
637 WazuhApiError::ApiError(
638 "Missing 'data.affected_items' in ports response".to_string(),
639 )
640 })?;
641
642 let ports: Vec<Port> = serde_json::from_value(ports_data.clone())?;
643 info!(%agent_id, "Retrieved {} ports", ports.len());
644 Ok(ports)
645 }
646
647 pub async fn get_vulnerability_summary(
648 &mut self,
649 agent_id: &str,
650 ) -> Result<Option<VulnerabilitySummaryResponseData>, WazuhApiError> {
651 debug!(%agent_id, "Getting vulnerability summary");
652
653 let query_body = json!({
654 "size": 0,
655 "query": {
656 "bool": {
657 "must": [
658 {
659 "term": {
660 "agent.id": agent_id
661 }
662 }
663 ]
664 }
665 },
666 "aggs": {
667 "severity_counts": {
668 "terms": {
669 "field": "vulnerability.severity",
670 "size": 10
671 }
672 }
673 }
674 });
675
676 let endpoint = "/wazuh-states-vulnerabilities*/_search";
677 match self
678 .indexer_client
679 .make_indexer_request(Method::POST, endpoint, Some(query_body))
680 .await
681 {
682 Ok(response_value) => {
683 let aggregations = response_value
684 .get("aggregations")
685 .and_then(|aggs| aggs.get("severity_counts"))
686 .and_then(|severity| severity.get("buckets"))
687 .and_then(|buckets| buckets.as_array());
688
689 if let Some(buckets) = aggregations {
690 let mut critical = 0;
691 let mut high = 0;
692 let mut medium = 0;
693 let mut low = 0;
694
695 for bucket in buckets {
696 if let (Some(key), Some(count)) = (
697 bucket.get("key").and_then(|k| k.as_str()),
698 bucket.get("doc_count").and_then(|c| c.as_i64()),
699 ) {
700 if let Some(severity) = VulnerabilitySeverity::parse(key) {
701 match severity {
702 VulnerabilitySeverity::Critical => critical = count as i32,
703 VulnerabilitySeverity::High => high = count as i32,
704 VulnerabilitySeverity::Medium => medium = count as i32,
705 VulnerabilitySeverity::Low => low = count as i32,
706 }
707 }
708 }
709 }
710
711 let summary = VulnerabilitySummaryResponseData {
712 agent_id: agent_id.to_string(),
713 summary: VulnerabilitySummaryCounts {
714 critical,
715 high,
716 medium,
717 low,
718 },
719 };
720
721 info!(%agent_id, "Retrieved vulnerability summary");
722 Ok(Some(summary))
723 } else {
724 info!(%agent_id, "No vulnerability data found for summary");
725 Ok(None)
726 }
727 }
728 Err(e) => {
729 warn!(%agent_id, "Failed to get vulnerability summary: {}", e);
730 Ok(None)
731 }
732 }
733 }
734
735 pub async fn search_package(
736 &mut self,
737 package_name: &str,
738 agent_ids: Option<&[String]>,
739 ) -> Result<Vec<(String, Vec<Package>)>, WazuhApiError> {
740 debug!(%package_name, "Searching for package across agents");
741
742 let mut results = Vec::new();
743
744 if let Some(agent_ids) = agent_ids {
745 for agent_id in agent_ids {
746 match self
747 .get_agent_packages(agent_id, None, None, Some(package_name))
748 .await
749 {
750 Ok(packages) => {
751 if !packages.is_empty() {
752 results.push((agent_id.clone(), packages));
753 }
754 }
755 Err(e) => {
756 debug!(%agent_id, %package_name, "Failed to get packages: {}", e);
757 }
758 }
759 }
760 }
761
762 info!(%package_name, "Found package in {} agents", results.len());
763 Ok(results)
764 }
765
766 pub async fn get_agents_with_vulnerability(
767 &mut self,
768 cve: &str,
769 agent_ids: &[String],
770 limit: Option<u32>,
771 ) -> Result<Vec<String>, WazuhApiError> {
772 debug!(%cve, "Finding agents with specific vulnerability");
773
774 let mut affected_agents = Vec::new();
775
776 for agent_id in agent_ids {
777 match self
778 .get_agent_vulnerabilities(agent_id, limit, None, None)
779 .await
780 {
781 Ok(vulnerabilities) => {
782 if vulnerabilities.iter().any(|v| v.cve == cve) {
783 affected_agents.push(agent_id.clone());
784 }
785 }
786 Err(e) => {
787 debug!(%agent_id, %cve, "Failed to get vulnerabilities: {}", e);
788 }
789 }
790 }
791
792 info!(%cve, "Found {} agents with vulnerability", affected_agents.len());
793 Ok(affected_agents)
794 }
795
796 pub async fn get_vulnerabilities_by_min_severity(
798 &mut self,
799 agent_id: &str,
800 min_severity: VulnerabilitySeverity,
801 limit: Option<u32>,
802 offset: Option<u32>,
803 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
804 debug!(%agent_id, ?min_severity, "Getting vulnerabilities with minimum severity");
805
806 let all_vulnerabilities = self
808 .get_agent_vulnerabilities(agent_id, limit, offset, None)
809 .await?;
810
811 let filtered: Vec<Vulnerability> = all_vulnerabilities
812 .into_iter()
813 .filter(|vuln| match (vuln.severity, min_severity) {
814 (VulnerabilitySeverity::Critical, _) => true,
815 (VulnerabilitySeverity::High, VulnerabilitySeverity::Critical) => false,
816 (VulnerabilitySeverity::High, _) => true,
817 (
818 VulnerabilitySeverity::Medium,
819 VulnerabilitySeverity::Critical | VulnerabilitySeverity::High,
820 ) => false,
821 (VulnerabilitySeverity::Medium, _) => true,
822 (VulnerabilitySeverity::Low, VulnerabilitySeverity::Low) => true,
823 (VulnerabilitySeverity::Low, _) => false,
824 })
825 .collect();
826
827 info!(%agent_id, ?min_severity, "Found {} vulnerabilities at or above severity level", filtered.len());
828 Ok(filtered)
829 }
830
831 pub async fn get_vulnerabilities_by_severities(
833 &mut self,
834 agent_id: &str,
835 severities: &[VulnerabilitySeverity],
836 limit: Option<u32>,
837 offset: Option<u32>,
838 ) -> Result<Vec<Vulnerability>, WazuhApiError> {
839 debug!(%agent_id, ?severities, "Getting vulnerabilities for multiple severity levels");
840
841 let mut all_vulnerabilities = Vec::new();
842
843 for &severity in severities {
844 let vulns = self
845 .get_agent_vulnerabilities(agent_id, limit, offset, Some(severity))
846 .await?;
847 all_vulnerabilities.extend(vulns);
848 }
849
850 all_vulnerabilities.sort_by(|a, b| a.cve.cmp(&b.cve));
852 all_vulnerabilities.dedup_by(|a, b| a.cve == b.cve);
853
854 info!(%agent_id, ?severities, "Found {} unique vulnerabilities", all_vulnerabilities.len());
855 Ok(all_vulnerabilities)
856 }
857}