wazuh_client/
active_response.rs

1use reqwest::Method;
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use tracing::{debug, info, warn};
5
6use super::error::WazuhApiError;
7use super::wazuh_client::WazuhApiClient;
8
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct ActiveResponseCommand {
11    pub name: String,
12    pub description: Option<String>,
13    pub command: String,
14    pub location: String,
15    pub timeout_allowed: Option<bool>,
16    pub expect: Option<String>,
17}
18
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct ActiveResponseExecution {
21    pub command: String,
22    pub arguments: Vec<String>,
23    pub alert: Option<Value>,
24    pub custom: Option<bool>,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
28pub struct ActiveResponseResult {
29    pub message: String,
30    pub error: Option<String>,
31    pub data: Option<Value>,
32}
33
34#[derive(Debug, Clone)]
35pub struct ActiveResponseClient {
36    api_client: WazuhApiClient,
37}
38
39impl ActiveResponseClient {
40    pub fn new(api_client: WazuhApiClient) -> Self {
41        Self { api_client }
42    }
43
44    pub async fn get_active_response_commands(
45        &mut self,
46    ) -> Result<Vec<ActiveResponseCommand>, WazuhApiError> {
47        debug!("Getting available active response commands");
48
49        let response = self
50            .api_client
51            .make_request(Method::GET, "/active-response", None, None)
52            .await?;
53
54        let commands_data = response
55            .get("data")
56            .and_then(|d| d.get("affected_items"))
57            .ok_or_else(|| {
58                WazuhApiError::ApiError(
59                    "Missing 'data.affected_items' in active response commands response"
60                        .to_string(),
61                )
62            })?;
63
64        let commands: Vec<ActiveResponseCommand> = serde_json::from_value(commands_data.clone())?;
65        info!("Retrieved {} active response commands", commands.len());
66        Ok(commands)
67    }
68
69    pub async fn execute_command_on_agent(
70        &mut self,
71        agent_id: &str,
72        command: &str,
73        arguments: Option<Vec<String>>,
74        custom: Option<bool>,
75        alert: Option<Value>,
76    ) -> Result<ActiveResponseResult, WazuhApiError> {
77        debug!(%agent_id, %command, ?arguments, "Executing active response command on agent");
78
79        let mut body = json!({
80            "command": command,
81            "agents_list": [agent_id]
82        });
83
84        if let Some(args) = arguments {
85            body["arguments"] = json!(args);
86        }
87
88        if let Some(custom_flag) = custom {
89            body["custom"] = json!(custom_flag);
90        }
91
92        if let Some(alert_data) = alert {
93            body["alert"] = alert_data;
94        }
95
96        let response = self
97            .api_client
98            .make_request(Method::PUT, "/active-response", Some(body), None)
99            .await?;
100
101        let result = ActiveResponseResult {
102            message: response
103                .get("message")
104                .and_then(|m| m.as_str())
105                .unwrap_or("Command executed")
106                .to_string(),
107            error: response
108                .get("error")
109                .and_then(|e| e.as_str())
110                .map(|s| s.to_string()),
111            data: response.get("data").cloned(),
112        };
113
114        info!(%agent_id, %command, "Active response command executed");
115        Ok(result)
116    }
117
118    pub async fn execute_command_on_agents(
119        &mut self,
120        agent_ids: &[String],
121        command: &str,
122        arguments: Option<Vec<String>>,
123        custom: Option<bool>,
124        alert: Option<Value>,
125    ) -> Result<ActiveResponseResult, WazuhApiError> {
126        debug!(?agent_ids, %command, ?arguments, "Executing active response command on multiple agents");
127
128        let mut body = json!({
129            "command": command,
130            "agents_list": agent_ids
131        });
132
133        if let Some(args) = arguments {
134            body["arguments"] = json!(args);
135        }
136
137        if let Some(custom_flag) = custom {
138            body["custom"] = json!(custom_flag);
139        }
140
141        if let Some(alert_data) = alert {
142            body["alert"] = alert_data;
143        }
144
145        let response = self
146            .api_client
147            .make_request(Method::PUT, "/active-response", Some(body), None)
148            .await?;
149
150        let result = ActiveResponseResult {
151            message: response
152                .get("message")
153                .and_then(|m| m.as_str())
154                .unwrap_or("Command executed")
155                .to_string(),
156            error: response
157                .get("error")
158                .and_then(|e| e.as_str())
159                .map(|s| s.to_string()),
160            data: response.get("data").cloned(),
161        };
162
163        info!(
164            "Active response command executed on {} agents",
165            agent_ids.len()
166        );
167        Ok(result)
168    }
169
170    pub async fn block_ip(
171        &mut self,
172        agent_id: &str,
173        ip_address: &str,
174        timeout: Option<u32>,
175    ) -> Result<ActiveResponseResult, WazuhApiError> {
176        debug!(%agent_id, %ip_address, ?timeout, "Blocking IP address");
177
178        let mut arguments = vec![ip_address.to_string()];
179        if let Some(timeout_val) = timeout {
180            arguments.push(timeout_val.to_string());
181        }
182
183        self.execute_command_on_agent(agent_id, "firewall-drop", Some(arguments), Some(true), None)
184            .await
185    }
186
187    pub async fn unblock_ip(
188        &mut self,
189        agent_id: &str,
190        ip_address: &str,
191    ) -> Result<ActiveResponseResult, WazuhApiError> {
192        debug!(%agent_id, %ip_address, "Unblocking IP address");
193
194        let arguments = vec![ip_address.to_string()];
195
196        self.execute_command_on_agent(agent_id, "firewall-drop", Some(arguments), Some(true), None)
197            .await
198    }
199
200    pub async fn isolate_host(
201        &mut self,
202        agent_id: &str,
203        interface: Option<&str>,
204    ) -> Result<ActiveResponseResult, WazuhApiError> {
205        debug!(%agent_id, ?interface, "Isolating host");
206
207        let arguments = if let Some(iface) = interface {
208            vec![iface.to_string()]
209        } else {
210            vec!["eth0".to_string()] // Default interface
211        };
212
213        self.execute_command_on_agent(
214            agent_id,
215            "host-isolate", // Changed command name to be more appropriate
216            Some(arguments),
217            Some(true),
218            None,
219        )
220        .await
221    }
222
223    pub async fn kill_process(
224        &mut self,
225        agent_id: &str,
226        pid: u32,
227    ) -> Result<ActiveResponseResult, WazuhApiError> {
228        debug!(%agent_id, %pid, "Killing process");
229
230        let arguments = vec![pid.to_string()];
231
232        self.execute_command_on_agent(agent_id, "kill", Some(arguments), Some(true), None)
233            .await
234    }
235
236    pub async fn disable_user_account(
237        &mut self,
238        agent_id: &str,
239        username: &str,
240    ) -> Result<ActiveResponseResult, WazuhApiError> {
241        debug!(%agent_id, %username, "Disabling user account");
242
243        let arguments = vec![username.to_string()];
244
245        self.execute_command_on_agent(
246            agent_id,
247            "disable-account",
248            Some(arguments),
249            Some(true),
250            None,
251        )
252        .await
253    }
254
255    pub async fn execute_custom_script(
256        &mut self,
257        agent_id: &str,
258        script_name: &str,
259        script_arguments: Option<Vec<String>>,
260    ) -> Result<ActiveResponseResult, WazuhApiError> {
261        debug!(%agent_id, %script_name, ?script_arguments, "Executing custom script");
262
263        let mut arguments = vec![script_name.to_string()];
264        if let Some(script_args) = script_arguments {
265            arguments.extend(script_args);
266        }
267
268        self.execute_command_on_agent(agent_id, "custom-script", Some(arguments), Some(true), None)
269            .await
270    }
271
272    pub async fn execute_response_for_alert(
273        &mut self,
274        agent_id: &str,
275        alert: Value,
276        command: &str,
277    ) -> Result<ActiveResponseResult, WazuhApiError> {
278        debug!(%agent_id, %command, "Executing active response for alert");
279
280        let mut arguments = Vec::new();
281
282        if let Some(src_ip) = alert
283            .get("data")
284            .and_then(|d| d.get("srcip"))
285            .and_then(|ip| ip.as_str())
286        {
287            arguments.push(src_ip.to_string());
288        }
289
290        if let Some(user) = alert
291            .get("data")
292            .and_then(|d| d.get("srcuser"))
293            .and_then(|u| u.as_str())
294        {
295            arguments.push(user.to_string());
296        }
297
298        self.execute_command_on_agent(
299            agent_id,
300            command,
301            if arguments.is_empty() {
302                None
303            } else {
304                Some(arguments)
305            },
306            Some(false),
307            Some(alert),
308        )
309        .await
310    }
311
312    pub async fn get_execution_history(
313        &mut self,
314        agent_id: Option<&str>,
315        limit: Option<u32>,
316    ) -> Result<Vec<Value>, WazuhApiError> {
317        debug!(
318            ?agent_id,
319            ?limit,
320            "Getting active response execution history"
321        );
322
323        let mut query_params = Vec::new();
324
325        if let Some(agent) = agent_id {
326            query_params.push(("agents_list", agent.to_string()));
327        }
328        if let Some(limit_val) = limit {
329            query_params.push(("limit", limit_val.to_string()));
330        }
331
332        let query_params_ref: Vec<(&str, &str)> =
333            query_params.iter().map(|(k, v)| (*k, v.as_str())).collect();
334
335        let response = self
336            .api_client
337            .make_request(
338                Method::GET,
339                "/active-response/history",
340                None,
341                if query_params_ref.is_empty() {
342                    None
343                } else {
344                    Some(&query_params_ref)
345                },
346            )
347            .await?;
348
349        let history_data = response
350            .get("data")
351            .and_then(|d| d.get("affected_items"))
352            .and_then(|items| items.as_array())
353            .ok_or_else(|| {
354                WazuhApiError::ApiError(
355                    "Missing 'data.affected_items' in active response history response".to_string(),
356                )
357            })?;
358
359        info!(
360            "Retrieved {} active response history entries",
361            history_data.len()
362        );
363        Ok(history_data.clone())
364    }
365
366    pub async fn validate_command(
367        &mut self,
368        command: &str,
369        arguments: Option<&[String]>,
370    ) -> Result<bool, WazuhApiError> {
371        debug!(%command, ?arguments, "Validating active response command");
372
373        let available_commands = self.get_active_response_commands().await?;
374
375        let command_exists = available_commands.iter().any(|cmd| cmd.name == command);
376
377        if !command_exists {
378            warn!(%command, "Active response command not found");
379            return Ok(false);
380        }
381
382        info!(%command, "Active response command validated successfully");
383        Ok(true)
384    }
385}