wazuh_client/
agents.rs

1use std::fmt::Display;
2
3use reqwest::Method;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use tracing::{debug, info};
7
8use super::error::WazuhApiError;
9use super::wazuh_client::WazuhApiClient;
10
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct Agent {
13    pub id: String,
14    pub name: String,
15    pub ip: Option<String>,
16    #[serde(rename = "registerIP")]
17    pub register_ip: Option<String>,
18    pub status: String,
19    #[serde(rename = "status_code")]
20    pub status_code: Option<i32>,
21    #[serde(rename = "configSum")]
22    pub config_sum: Option<String>,
23    #[serde(rename = "mergedSum")]
24    pub merged_sum: Option<String>,
25    #[serde(rename = "dateAdd")]
26    pub date_add: Option<String>,
27    #[serde(rename = "lastKeepAlive")]
28    pub last_keep_alive: Option<String>,
29    pub os: Option<AgentOs>,
30    pub version: Option<String>,
31    pub manager: Option<String>,
32    pub group: Option<Vec<String>>,
33    pub node_name: Option<String>,
34    #[serde(rename = "group_config_status")]
35    pub group_config_status: Option<String>,
36}
37
38#[derive(Debug, Clone, Deserialize, Serialize)]
39pub struct AgentOs {
40    pub arch: Option<String>,
41    pub major: Option<String>,
42    pub minor: Option<String>,
43    pub name: Option<String>,
44    pub platform: Option<String>,
45    pub uname: Option<String>,
46    pub version: Option<String>,
47    pub codename: Option<String>,
48}
49
50impl Display for AgentOs {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(
53            f,
54            "{} {} {}",
55            self.platform.as_deref().unwrap_or("unknown"),
56            self.version.as_deref().unwrap_or("unknown"),
57            self.arch.as_deref().unwrap_or("unknown")
58        )
59    }
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
63pub struct AgentConnectionSummary {
64    pub total: u32,
65    pub active: u32,
66    pub disconnected: u32,
67    #[serde(rename = "never_connected")]
68    pub never_connected: u32,
69    pub pending: u32,
70}
71
72#[derive(Debug, Clone, Deserialize, Serialize)]
73pub struct AgentConfigurationSummary {
74    pub total: u32,
75    pub synced: u32,
76    #[serde(rename = "not_synced")]
77    pub not_synced: u32,
78}
79
80#[derive(Debug, Clone, Deserialize, Serialize)]
81pub struct AgentSummary {
82    pub connection: AgentConnectionSummary,
83    pub configuration: AgentConfigurationSummary,
84}
85
86#[derive(Debug, Clone, Deserialize, Serialize)]
87pub struct AgentKey {
88    pub id: String,
89    pub key: String,
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize)]
93pub struct AgentAddBody {
94    pub name: String,
95    pub ip: Option<String>,
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize)]
99pub struct AgentInsertBody {
100    pub name: String,
101    pub ip: Option<String>,
102    pub id: Option<String>,
103    pub key: Option<String>,
104    pub force: Option<AgentForceOptions>,
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
108pub struct AgentForceOptions {
109    pub enabled: bool,
110    pub disconnected_time: Option<AgentDisconnectedTime>,
111    pub after_registration_time: Option<String>,
112}
113
114#[derive(Debug, Clone, Deserialize, Serialize)]
115pub struct AgentDisconnectedTime {
116    pub enabled: bool,
117    pub value: Option<String>,
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct AgentIdKey {
122    pub id: String,
123    pub key: String,
124}
125
126#[derive(Debug, Clone)]
127pub struct AgentsClient {
128    api_client: WazuhApiClient,
129}
130
131impl AgentsClient {
132    pub fn new(api_client: WazuhApiClient) -> Self {
133        Self { api_client }
134    }
135
136    #[allow(clippy::too_many_arguments)]
137    pub async fn get_agents(
138        &mut self,
139        limit: Option<u32>,
140        offset: Option<u32>,
141        select: Option<&str>,
142        sort: Option<&str>,
143        search: Option<&str>,
144        status: Option<&str>,
145        query: Option<&str>,
146        older_than: Option<&str>,
147        os_platform: Option<&str>,
148        os_version: Option<&str>,
149        os_name: Option<&str>,
150        manager_host: Option<&str>,
151        version: Option<&str>,
152        group: Option<&str>,
153        node_name: Option<&str>,
154        name: Option<&str>,
155        ip: Option<&str>,
156        register_ip: Option<&str>,
157        group_config_status: Option<&str>,
158        distinct: Option<bool>,
159    ) -> Result<Vec<Agent>, WazuhApiError> {
160        debug!("Getting agents list with comprehensive filters");
161
162        let mut query_params = Vec::new();
163
164        if let Some(limit) = limit {
165            query_params.push(("limit", limit.to_string()));
166        }
167        if let Some(offset) = offset {
168            query_params.push(("offset", offset.to_string()));
169        }
170        if let Some(select) = select {
171            query_params.push(("select", select.to_string()));
172        }
173        if let Some(sort) = sort {
174            query_params.push(("sort", sort.to_string()));
175        }
176        if let Some(search) = search {
177            query_params.push(("search", search.to_string()));
178        }
179        if let Some(status) = status {
180            query_params.push(("status", status.to_string()));
181        }
182        if let Some(query) = query {
183            query_params.push(("q", query.to_string()));
184        }
185        if let Some(older_than) = older_than {
186            query_params.push(("older_than", older_than.to_string()));
187        }
188        if let Some(os_platform) = os_platform {
189            query_params.push(("os.platform", os_platform.to_string()));
190        }
191        if let Some(os_version) = os_version {
192            query_params.push(("os.version", os_version.to_string()));
193        }
194        if let Some(os_name) = os_name {
195            query_params.push(("os.name", os_name.to_string()));
196        }
197        if let Some(manager_host) = manager_host {
198            query_params.push(("manager_host", manager_host.to_string()));
199        }
200        if let Some(version) = version {
201            query_params.push(("version", version.to_string()));
202        }
203        if let Some(group) = group {
204            query_params.push(("group", group.to_string()));
205        }
206        if let Some(node_name) = node_name {
207            query_params.push(("node_name", node_name.to_string()));
208        }
209        if let Some(name) = name {
210            query_params.push(("name", name.to_string()));
211        }
212        if let Some(ip) = ip {
213            query_params.push(("ip", ip.to_string()));
214        }
215        if let Some(register_ip) = register_ip {
216            query_params.push(("registerIP", register_ip.to_string()));
217        }
218        if let Some(group_config_status) = group_config_status {
219            query_params.push(("group_config_status", group_config_status.to_string()));
220        }
221        if let Some(distinct) = distinct {
222            query_params.push(("distinct", distinct.to_string()));
223        }
224
225        let query_params_ref: Vec<(&str, &str)> =
226            query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
227
228        let response = self
229            .api_client
230            .make_request(
231                Method::GET,
232                "/agents",
233                None,
234                if query_params_ref.is_empty() {
235                    None
236                } else {
237                    Some(&query_params_ref)
238                },
239            )
240            .await?;
241
242        let agents_data = response
243            .get("data")
244            .and_then(|d| d.get("affected_items"))
245            .ok_or_else(|| {
246                WazuhApiError::ApiError(
247                    "Missing 'data.affected_items' in agents response".to_string(),
248                )
249            })?;
250
251        let agents: Vec<Agent> = serde_json::from_value(agents_data.clone())?;
252        info!("Retrieved {} agents", agents.len());
253        Ok(agents)
254    }
255
256    pub async fn get_agent(&mut self, agent_id: &str) -> Result<Agent, WazuhApiError> {
257        debug!(%agent_id, "Getting specific agent");
258
259        let endpoint = format!("/agents/{}", agent_id);
260        let response = self
261            .api_client
262            .make_request(Method::GET, &endpoint, None, None)
263            .await?;
264
265        let agent_data = response
266            .get("data")
267            .and_then(|d| d.get("affected_items"))
268            .and_then(|items| items.as_array())
269            .and_then(|arr| arr.first())
270            .ok_or_else(|| WazuhApiError::ApiError(format!("Agent {} not found", agent_id)))?;
271
272        let agent: Agent = serde_json::from_value(agent_data.clone())?;
273        info!(%agent_id, "Retrieved agent details");
274        Ok(agent)
275    }
276
277    pub async fn get_agents_summary(&mut self) -> Result<AgentSummary, WazuhApiError> {
278        debug!("Getting agents summary");
279
280        let response = self
281            .api_client
282            .make_request(Method::GET, "/agents/summary/status", None, None)
283            .await?;
284
285        let summary_data = response.get("data").ok_or_else(|| {
286            WazuhApiError::ApiError("Missing 'data' in agents summary response".to_string())
287        })?;
288
289        let summary: AgentSummary = serde_json::from_value(summary_data.clone())?;
290        info!(
291            "Retrieved agents summary: {} total agents",
292            summary.connection.total
293        );
294        Ok(summary)
295    }
296
297    pub async fn get_agent_key(&mut self, agent_id: &str) -> Result<AgentKey, WazuhApiError> {
298        debug!(%agent_id, "Getting agent key");
299
300        let endpoint = format!("/agents/{}/key", agent_id);
301        let response = self
302            .api_client
303            .make_request(Method::GET, &endpoint, None, None)
304            .await?;
305
306        let key_data = response
307            .get("data")
308            .and_then(|d| d.get("affected_items"))
309            .and_then(|items| items.as_array())
310            .and_then(|arr| arr.first())
311            .ok_or_else(|| {
312                WazuhApiError::ApiError(format!("Agent key for {} not found", agent_id))
313            })?;
314
315        let agent_key: AgentKey = serde_json::from_value(key_data.clone())?;
316        info!(%agent_id, "Retrieved agent key");
317        Ok(agent_key)
318    }
319
320    pub async fn get_agent_config(
321        &mut self,
322        agent_id: &str,
323        component: &str,
324        configuration: &str,
325    ) -> Result<Value, WazuhApiError> {
326        debug!(%agent_id, %component, %configuration, "Getting agent configuration");
327
328        let endpoint = format!(
329            "/agents/{}/config/{}/{}",
330            agent_id, component, configuration
331        );
332        let response = self
333            .api_client
334            .make_request(Method::GET, &endpoint, None, None)
335            .await?;
336
337        let config_data = response.get("data").ok_or_else(|| {
338            WazuhApiError::ApiError("Missing 'data' in agent config response".to_string())
339        })?;
340
341        info!(%agent_id, %component, %configuration, "Retrieved agent configuration");
342        Ok(config_data.clone())
343    }
344
345    pub async fn add_agent(
346        &mut self,
347        agent_data: AgentAddBody,
348    ) -> Result<AgentIdKey, WazuhApiError> {
349        debug!(?agent_data, "Adding new agent");
350
351        let body = serde_json::to_value(agent_data)?;
352        let response = self
353            .api_client
354            .make_request(Method::POST, "/agents", Some(body), None)
355            .await?;
356
357        let agent_id_key_data = response.get("data").ok_or_else(|| {
358            WazuhApiError::ApiError("Missing 'data' in add agent response".to_string())
359        })?;
360
361        let agent_id_key: AgentIdKey = serde_json::from_value(agent_id_key_data.clone())?;
362        info!(agent_id = %agent_id_key.id, "Agent added successfully");
363        Ok(agent_id_key)
364    }
365
366    pub async fn insert_agent(
367        &mut self,
368        agent_data: AgentInsertBody,
369    ) -> Result<AgentIdKey, WazuhApiError> {
370        debug!(?agent_data, "Inserting agent with full details");
371
372        let body = serde_json::to_value(agent_data)?;
373        let response = self
374            .api_client
375            .make_request(Method::POST, "/agents/insert", Some(body), None)
376            .await?;
377
378        let agent_id_key_data = response.get("data").ok_or_else(|| {
379            WazuhApiError::ApiError("Missing 'data' in insert agent response".to_string())
380        })?;
381
382        let agent_id_key: AgentIdKey = serde_json::from_value(agent_id_key_data.clone())?;
383        info!(agent_id = %agent_id_key.id, "Agent inserted successfully");
384        Ok(agent_id_key)
385    }
386
387    pub async fn add_agent_quick(&mut self, agent_name: &str) -> Result<AgentIdKey, WazuhApiError> {
388        debug!(%agent_name, "Quick adding agent");
389
390        let query_params = [("agent_name", agent_name)];
391        let response = self
392            .api_client
393            .make_request(
394                Method::POST,
395                "/agents/insert/quick",
396                None,
397                Some(&query_params),
398            )
399            .await?;
400
401        let agent_id_key_data = response.get("data").ok_or_else(|| {
402            WazuhApiError::ApiError("Missing 'data' in quick add agent response".to_string())
403        })?;
404
405        let agent_id_key: AgentIdKey = serde_json::from_value(agent_id_key_data.clone())?;
406        info!(agent_id = %agent_id_key.id, %agent_name, "Agent quick added successfully");
407        Ok(agent_id_key)
408    }
409
410    pub async fn delete_agents(&mut self, agent_ids: &[String]) -> Result<Value, WazuhApiError> {
411        debug!(?agent_ids, "Deleting agents");
412
413        let agents_list = agent_ids.join(",");
414        let query_params = [("agents_list", agents_list.as_str())];
415
416        let response = self
417            .api_client
418            .make_request(Method::DELETE, "/agents", None, Some(&query_params))
419            .await?;
420
421        info!("Deleted {} agents", agent_ids.len());
422        Ok(response)
423    }
424
425    pub async fn get_agents_no_group(&mut self) -> Result<Vec<Agent>, WazuhApiError> {
426        debug!("Getting agents without group");
427
428        let response = self
429            .api_client
430            .make_request(Method::GET, "/agents/no_group", None, None)
431            .await?;
432
433        let agents_data = response
434            .get("data")
435            .and_then(|d| d.get("affected_items"))
436            .ok_or_else(|| {
437                WazuhApiError::ApiError(
438                    "Missing 'data.affected_items' in agents no group response".to_string(),
439                )
440            })?;
441
442        let agents: Vec<Agent> = serde_json::from_value(agents_data.clone())?;
443        info!("Retrieved {} agents without group", agents.len());
444        Ok(agents)
445    }
446
447    pub async fn get_agent_group_sync_status(
448        &mut self,
449        agent_id: &str,
450    ) -> Result<Value, WazuhApiError> {
451        debug!(%agent_id, "Checking agent group sync status");
452
453        let endpoint = format!("/agents/{}/group/is_sync", agent_id);
454        let response = self
455            .api_client
456            .make_request(Method::GET, &endpoint, None, None)
457            .await?;
458
459        info!(%agent_id, "Retrieved agent group sync status");
460        Ok(response)
461    }
462
463    pub async fn remove_agent_from_group(
464        &mut self,
465        agent_id: &str,
466        group_id: Option<&str>,
467    ) -> Result<Value, WazuhApiError> {
468        let endpoint = if let Some(group_id) = group_id {
469            debug!(%agent_id, %group_id, "Removing agent from specific group");
470            format!("/agents/{}/group/{}", agent_id, group_id)
471        } else {
472            debug!(%agent_id, "Removing agent from all groups");
473            format!("/agents/{}/group", agent_id)
474        };
475
476        let response = self
477            .api_client
478            .make_request(Method::DELETE, &endpoint, None, None)
479            .await?;
480
481        info!(%agent_id, "Agent removed from group(s)");
482        Ok(response)
483    }
484
485    pub async fn get_outdated_agents(&mut self) -> Result<Vec<Agent>, WazuhApiError> {
486        debug!("Getting outdated agents");
487
488        let response = self
489            .api_client
490            .make_request(Method::GET, "/agents/outdated", None, None)
491            .await?;
492
493        let agents_data = response
494            .get("data")
495            .and_then(|d| d.get("affected_items"))
496            .ok_or_else(|| {
497                WazuhApiError::ApiError(
498                    "Missing 'data.affected_items' in outdated agents response".to_string(),
499                )
500            })?;
501
502        let agents: Vec<Agent> = serde_json::from_value(agents_data.clone())?;
503        info!("Retrieved {} outdated agents", agents.len());
504        Ok(agents)
505    }
506
507    pub async fn restart_agent(&mut self, agent_id: &str) -> Result<Value, WazuhApiError> {
508        debug!(%agent_id, "Restarting agent");
509
510        let endpoint = format!("/agents/{}/restart", agent_id);
511        let response = self
512            .api_client
513            .make_request(Method::PUT, &endpoint, None, None)
514            .await?;
515
516        info!(%agent_id, "Agent restart command sent");
517        Ok(response)
518    }
519
520    pub async fn restart_agents(&mut self, agent_ids: &[String]) -> Result<Value, WazuhApiError> {
521        debug!(?agent_ids, "Restarting multiple agents");
522
523        let body = json!({
524            "agents_list": agent_ids
525        });
526
527        let response = self
528            .api_client
529            .make_request(Method::PUT, "/agents/restart", Some(body), None)
530            .await?;
531
532        info!("Restart command sent to {} agents", agent_ids.len());
533        Ok(response)
534    }
535
536    pub async fn reconnect_agents(&mut self, agent_ids: &[String]) -> Result<Value, WazuhApiError> {
537        debug!(?agent_ids, "Reconnecting agents");
538
539        let agents_list = agent_ids.join(",");
540        let query_params = [("agents_list", agents_list.as_str())];
541
542        let response = self
543            .api_client
544            .make_request(Method::PUT, "/agents/reconnect", None, Some(&query_params))
545            .await?;
546
547        info!("Reconnect command sent to {} agents", agent_ids.len());
548        Ok(response)
549    }
550
551    pub async fn upgrade_agents(&mut self, agent_ids: &[String]) -> Result<Value, WazuhApiError> {
552        debug!(?agent_ids, "Upgrading agents");
553
554        let agents_list = agent_ids.join(",");
555        let query_params = [("agents_list", agents_list.as_str())];
556
557        let response = self
558            .api_client
559            .make_request(Method::PUT, "/agents/upgrade", None, Some(&query_params))
560            .await?;
561
562        info!("Upgrade command sent to {} agents", agent_ids.len());
563        Ok(response)
564    }
565
566    pub async fn upgrade_agents_custom(
567        &mut self,
568        agent_ids: &[String],
569        custom_params: Value,
570    ) -> Result<Value, WazuhApiError> {
571        debug!(?agent_ids, "Custom upgrading agents");
572
573        let agents_list = agent_ids.join(",");
574        let query_params = [("agents_list", agents_list.as_str())];
575
576        let response = self
577            .api_client
578            .make_request(
579                Method::PUT,
580                "/agents/upgrade_custom",
581                Some(custom_params),
582                Some(&query_params),
583            )
584            .await?;
585
586        info!("Custom upgrade command sent to {} agents", agent_ids.len());
587        Ok(response)
588    }
589
590    pub async fn get_upgrade_results(
591        &mut self,
592        agent_ids: &[String],
593    ) -> Result<Value, WazuhApiError> {
594        debug!(?agent_ids, "Getting upgrade results");
595
596        let agents_list = agent_ids.join(",");
597        let query_params = [("agents_list", agents_list.as_str())];
598
599        let response = self
600            .api_client
601            .make_request(
602                Method::GET,
603                "/agents/upgrade_result",
604                None,
605                Some(&query_params),
606            )
607            .await?;
608
609        info!("Retrieved upgrade results for {} agents", agent_ids.len());
610        Ok(response)
611    }
612
613    pub async fn get_agent_daemon_stats(&mut self, agent_id: &str) -> Result<Value, WazuhApiError> {
614        debug!(%agent_id, "Getting agent daemon stats");
615
616        let endpoint = format!("/agents/{}/daemons/stats", agent_id);
617        let response = self
618            .api_client
619            .make_request(Method::GET, &endpoint, None, None)
620            .await?;
621
622        info!(%agent_id, "Retrieved agent daemon stats");
623        Ok(response)
624    }
625
626    pub async fn get_agent_component_stats(
627        &mut self,
628        agent_id: &str,
629        component: &str,
630    ) -> Result<Value, WazuhApiError> {
631        debug!(%agent_id, %component, "Getting agent component stats");
632
633        let endpoint = format!("/agents/{}/stats/{}", agent_id, component);
634        let response = self
635            .api_client
636            .make_request(Method::GET, &endpoint, None, None)
637            .await?;
638
639        info!(%agent_id, %component, "Retrieved agent component stats");
640        Ok(response)
641    }
642
643    pub async fn get_agents_by_group(
644        &mut self,
645        group_name: &str,
646    ) -> Result<Vec<Agent>, WazuhApiError> {
647        debug!(%group_name, "Getting agents by group");
648
649        let query_params = [("group", group_name)];
650        let response = self
651            .api_client
652            .make_request(Method::GET, "/agents", None, Some(&query_params))
653            .await?;
654
655        let agents_data = response
656            .get("data")
657            .and_then(|d| d.get("affected_items"))
658            .ok_or_else(|| {
659                WazuhApiError::ApiError(
660                    "Missing 'data.affected_items' in agents by group response".to_string(),
661                )
662            })?;
663
664        let agents: Vec<Agent> = serde_json::from_value(agents_data.clone())?;
665        info!(%group_name, "Retrieved {} agents from group", agents.len());
666        Ok(agents)
667    }
668
669    pub async fn get_disconnected_agents(&mut self) -> Result<Vec<Agent>, WazuhApiError> {
670        debug!("Getting disconnected agents");
671        self.get_agents(
672            None,
673            None,
674            None,
675            None,
676            None,
677            Some("disconnected"),
678            None,
679            None,
680            None,
681            None,
682            None,
683            None,
684            None,
685            None,
686            None,
687            None,
688            None,
689            None,
690            None,
691            None,
692        )
693        .await
694    }
695
696    pub async fn get_active_agents(&mut self) -> Result<Vec<Agent>, WazuhApiError> {
697        debug!("Getting active agents");
698        self.get_agents(
699            None,
700            None,
701            None,
702            None,
703            None,
704            Some("active"),
705            None,
706            None,
707            None,
708            None,
709            None,
710            None,
711            None,
712            None,
713            None,
714            None,
715            None,
716            None,
717            None,
718            None,
719        )
720        .await
721    }
722
723    pub async fn get_pending_agents(&mut self) -> Result<Vec<Agent>, WazuhApiError> {
724        debug!("Getting pending agents");
725        self.get_agents(
726            None,
727            None,
728            None,
729            None,
730            None,
731            Some("pending"),
732            None,
733            None,
734            None,
735            None,
736            None,
737            None,
738            None,
739            None,
740            None,
741            None,
742            None,
743            None,
744            None,
745            None,
746        )
747        .await
748    }
749
750    pub async fn get_never_connected_agents(&mut self) -> Result<Vec<Agent>, WazuhApiError> {
751        debug!("Getting never connected agents");
752        self.get_agents(
753            None,
754            None,
755            None,
756            None,
757            None,
758            Some("never_connected"),
759            None,
760            None,
761            None,
762            None,
763            None,
764            None,
765            None,
766            None,
767            None,
768            None,
769            None,
770            None,
771            None,
772            None,
773        )
774        .await
775    }
776}