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}