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()] };
212
213 self.execute_command_on_agent(
214 agent_id,
215 "host-isolate", 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}