1use alloc::{
5 string::{String, ToString},
6 vec::Vec,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12use super::{
13 ACTION_GET_TASKING,
14 peer::{
15 AlertMessage, DelegateMessage, EdgeMessage, InteractiveMessage, ReversePortForwardMessage,
16 SocksMessage,
17 },
18};
19
20fn default_tasking_size() -> u32 {
21 1
22}
23
24fn default_get_delegate_tasks() -> bool {
25 true
26}
27
28fn default_is_screenshot() -> bool {
29 false
30}
31
32fn is_false(value: &bool) -> bool {
33 !*value
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
39pub struct AgentExtras {
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub delegates: Vec<DelegateMessage>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub socks: Vec<SocksMessage>,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub rpfwd: Vec<ReversePortForwardMessage>,
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub interactive: Vec<InteractiveMessage>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub alerts: Vec<AlertMessage>,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
51 pub edges: Vec<EdgeMessage>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
56pub struct AgentMessageExtras {
57 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub responses: Vec<TaskResponse>,
59 #[serde(flatten)]
60 pub shared: AgentExtras,
61}
62
63pub type AgentResponseExtras = AgentExtras;
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct ReqGetTasking {
70 pub action: String,
71 #[serde(default = "default_tasking_size")]
72 pub tasking_size: u32,
73 #[serde(default = "default_get_delegate_tasks")]
74 pub get_delegate_tasks: bool,
75 #[serde(flatten)]
76 pub extras: AgentMessageExtras,
77}
78
79impl ReqGetTasking {
80 pub fn new(tasking_size: u32) -> Self {
81 Self {
82 action: ACTION_GET_TASKING.to_string(),
83 tasking_size,
84 get_delegate_tasks: true,
85 extras: AgentMessageExtras::default(),
86 }
87 }
88
89 pub fn with_delegate_tasks(tasking_size: u32, get_delegate_tasks: bool) -> Self {
90 Self {
91 action: ACTION_GET_TASKING.to_string(),
92 tasking_size,
93 get_delegate_tasks,
94 extras: AgentMessageExtras::default(),
95 }
96 }
97
98 pub fn with_extras(tasking_size: u32, extras: AgentMessageExtras) -> Self {
101 Self {
102 action: ACTION_GET_TASKING.to_string(),
103 tasking_size,
104 get_delegate_tasks: true,
105 extras,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
111pub struct RespGetTasking {
112 pub action: String,
113 #[serde(default)]
114 pub tasks: Vec<TaskMessage>,
115 #[serde(flatten)]
116 pub extras: AgentResponseExtras,
117}
118
119impl RespGetTasking {
120 pub fn new(tasks: Vec<TaskMessage>) -> Self {
121 Self {
122 action: ACTION_GET_TASKING.to_string(),
123 tasks,
124 extras: AgentResponseExtras::default(),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
132pub struct TaskMessage {
133 pub command: String,
134 pub parameters: String,
135 pub timestamp: f64,
136 pub id: Uuid,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
145pub struct TaskResponse {
146 pub task_id: Uuid,
147 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub completed: Option<bool>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub status: Option<String>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub user_output: Option<String>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub process_response: Option<Value>,
155
156 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub download: Option<TaskDownload>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub upload: Option<TaskUpload>,
161
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub file_browser: Option<FileBrowserEntry>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub credentials: Vec<Credential>,
167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub artifacts: Vec<Artifact>,
169 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub processes: Vec<ProcessEntry>,
171 #[serde(default, skip_serializing_if = "Vec::is_empty")]
172 pub commands: Vec<CommandAction>,
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub keylogs: Vec<KeylogEntry>,
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub tokens: Vec<TokenEntry>,
177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub callback_tokens: Vec<CallbackToken>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub removed_files: Vec<RemovedFileInfo>,
181
182 #[serde(default, skip_serializing_if = "Vec::is_empty")]
184 pub alerts: Vec<AlertMessage>,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
186 pub edges: Vec<EdgeMessage>,
187 #[serde(default, skip_serializing_if = "Vec::is_empty")]
188 pub socks: Vec<SocksMessage>,
189 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub rpfwd: Vec<ReversePortForwardMessage>,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 pub interactive: Vec<InteractiveMessage>,
193}
194
195impl TaskResponse {
196 pub fn completed(task_id: Uuid, user_output: &str) -> Self {
197 Self {
198 task_id,
199 completed: Some(true),
200 status: Some("completed".into()),
201 user_output: Some(user_output.into()),
202 ..Default::default()
203 }
204 }
205
206 pub fn failed(task_id: Uuid, error: &str) -> Self {
207 Self {
208 task_id,
209 completed: Some(true),
210 status: Some("error".into()),
211 user_output: Some(error.into()),
212 ..Default::default()
213 }
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
220pub struct TaskDownload {
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub total_chunks: Option<u32>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub chunk_size: Option<u32>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub filename: Option<String>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub full_path: Option<String>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub host: Option<String>,
231 #[serde(default = "default_is_screenshot", skip_serializing_if = "is_false")]
232 pub is_screenshot: bool,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub file_id: Option<Uuid>,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub chunk_num: Option<u32>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub chunk_data: Option<String>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
243pub struct TaskUpload {
244 pub chunk_size: u32,
245 pub file_id: Uuid,
246 pub chunk_num: u32,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub full_path: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub host: Option<String>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
258pub struct FileBrowserEntry {
259 pub is_file: bool,
260 pub name: String,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub permissions: Option<Value>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub access_time: Option<i64>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub modify_time: Option<i64>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub size: Option<i64>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub host: Option<String>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub parent_path: Option<String>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub success: Option<bool>,
275 #[serde(default, skip_serializing_if = "is_false")]
276 pub update_deleted: bool,
277 #[serde(default, skip_serializing_if = "is_false")]
278 pub set_as_user_output: bool,
279 #[serde(default, skip_serializing_if = "Vec::is_empty")]
280 pub files: Vec<FileBrowserEntry>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
284pub struct Credential {
285 pub credential_type: String,
286 pub credential: String,
287 pub account: String,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub realm: Option<String>,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub comment: Option<String>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
295pub struct Artifact {
296 pub base_artifact: String,
297 pub artifact: String,
298 #[serde(default)]
299 pub needs_cleanup: bool,
300 #[serde(default)]
301 pub resolved: bool,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
305pub struct ProcessEntry {
306 pub process_id: i64,
307 pub name: String,
308 pub host: String,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub parent_process_id: Option<i64>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub architecture: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub bin_path: Option<String>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub user: Option<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub command_line: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub integrity_level: Option<i32>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub start_time: Option<i64>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub description: Option<String>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub signer: Option<String>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub protected_process_level: Option<i32>,
329 #[serde(default, skip_serializing_if = "is_false")]
330 pub update_deleted: bool,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
334pub struct CommandAction {
335 pub action: String,
336 pub cmd: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
340pub struct KeylogEntry {
341 pub keystrokes: String,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub user: Option<String>,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub window_title: Option<String>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
349pub struct TokenEntry {
350 pub token_id: i64,
351 pub host: String,
352 pub user: String,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub groups: Option<String>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub thread_id: Option<i64>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub process_id: Option<i64>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub default_dacl: Option<String>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub session_id: Option<i64>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub restricted: Option<bool>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub capabilities: Option<String>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub logon_sid: Option<String>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub integrity_level_sid: Option<i64>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub app_container_number: Option<i64>,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub app_container_sid: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub privileges: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub handle: Option<i64>,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
382pub struct CallbackToken {
383 pub action: String,
384 pub host: String,
385 pub token_id: i64,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
389pub struct RemovedFileInfo {
390 pub host: String,
391 pub path: String,
392}
393
394#[cfg(test)]
397mod tests {
398 use super::*;
399 use alloc::{string::ToString, vec};
400
401 use crate::protocol::peer::{
402 AlertMessage, EdgeMessage, InteractiveMessage, ReversePortForwardMessage, SocksMessage,
403 };
404
405 #[test]
406 fn get_tasking_defaults_are_correct() {
407 let req = ReqGetTasking::new(9);
408 let req_without = ReqGetTasking::with_delegate_tasks(3, false);
409
410 assert_eq!(req.action, ACTION_GET_TASKING);
411 assert_eq!(req.tasking_size, 9);
412 assert!(req.get_delegate_tasks);
413 assert!(!req_without.get_delegate_tasks);
414 }
415
416 #[test]
417 fn extras_roundtrip() {
418 let extras = AgentMessageExtras::default();
419 assert_eq!(
420 serde_json::from_str::<AgentMessageExtras>(&serde_json::to_string(&extras).unwrap())
421 .unwrap(),
422 extras
423 );
424
425 let resp_extras = AgentResponseExtras::default();
426 assert_eq!(
427 serde_json::from_str::<AgentResponseExtras>(
428 &serde_json::to_string(&resp_extras).unwrap()
429 )
430 .unwrap(),
431 resp_extras
432 );
433 }
434
435 #[test]
436 fn resp_get_tasking_roundtrip() {
437 let uuid = Uuid::nil();
438 let resp = RespGetTasking {
439 action: ACTION_GET_TASKING.to_string(),
440 tasks: vec![TaskMessage {
441 command: "ls".to_string(),
442 parameters: "-la".to_string(),
443 timestamp: 1.0,
444 id: uuid,
445 }],
446 extras: AgentResponseExtras::default(),
447 };
448 assert_eq!(
449 serde_json::from_str::<RespGetTasking>(&serde_json::to_string(&resp).unwrap()).unwrap(),
450 resp
451 );
452 }
453
454 #[test]
455 fn task_response_default_is_empty() {
456 let resp = TaskResponse::default();
457 assert!(resp.user_output.is_none());
458 assert!(resp.download.is_none());
459 assert!(resp.upload.is_none());
460 assert!(resp.file_browser.is_none());
461 assert!(resp.credentials.is_empty());
462 assert!(resp.artifacts.is_empty());
463 assert!(resp.processes.is_empty());
464 assert!(resp.commands.is_empty());
465 assert!(resp.keylogs.is_empty());
466 assert!(resp.tokens.is_empty());
467 assert!(resp.callback_tokens.is_empty());
468 assert!(resp.removed_files.is_empty());
469 }
470
471 #[test]
472 fn task_models_roundtrip() {
473 let uuid = Uuid::nil();
474
475 let task_message = TaskMessage {
476 command: "ls".to_string(),
477 parameters: "-la".to_string(),
478 timestamp: 1.5,
479 id: uuid,
480 };
481 assert_eq!(
482 serde_json::from_str::<TaskMessage>(&serde_json::to_string(&task_message).unwrap())
483 .unwrap(),
484 task_message
485 );
486
487 let task_download = TaskDownload {
488 total_chunks: Some(2),
489 chunk_size: Some(64),
490 filename: Some("out.txt".to_string()),
491 full_path: Some("/tmp/out.txt".to_string()),
492 host: Some("host-a".to_string()),
493 is_screenshot: false,
494 file_id: None,
495 chunk_num: None,
496 chunk_data: None,
497 };
498 assert_eq!(
499 serde_json::from_str::<TaskDownload>(&serde_json::to_string(&task_download).unwrap())
500 .unwrap(),
501 task_download
502 );
503
504 let task_upload = TaskUpload {
505 chunk_size: 512000,
506 file_id: uuid,
507 chunk_num: 1,
508 full_path: Some("/tmp/target".into()),
509 host: Some("host-a".into()),
510 };
511 assert_eq!(
512 serde_json::from_str::<TaskUpload>(&serde_json::to_string(&task_upload).unwrap())
513 .unwrap(),
514 task_upload
515 );
516
517 let file_entry = FileBrowserEntry {
518 is_file: false,
519 name: "dir".into(),
520 host: Some("h".into()),
521 parent_path: Some("/".into()),
522 success: Some(true),
523 permissions: Some(serde_json::json!({"x": 1})),
524 files: vec![FileBrowserEntry {
525 is_file: true,
526 name: "f.txt".into(),
527 size: Some(100),
528 ..Default::default()
529 }],
530 ..Default::default()
531 };
532 assert_eq!(
533 serde_json::from_str::<FileBrowserEntry>(&serde_json::to_string(&file_entry).unwrap())
534 .unwrap(),
535 file_entry
536 );
537
538 let credential = Credential {
539 credential_type: "plaintext".into(),
540 credential: "pass123".into(),
541 account: "admin".into(),
542 realm: Some("DOMAIN".into()),
543 comment: None,
544 };
545 assert_eq!(
546 serde_json::from_str::<Credential>(&serde_json::to_string(&credential).unwrap())
547 .unwrap(),
548 credential
549 );
550
551 let artifact = Artifact {
552 base_artifact: "Process Create".into(),
553 artifact: "sh -c whoami".into(),
554 needs_cleanup: false,
555 resolved: false,
556 };
557 assert_eq!(
558 serde_json::from_str::<Artifact>(&serde_json::to_string(&artifact).unwrap()).unwrap(),
559 artifact
560 );
561
562 let process = ProcessEntry {
563 process_id: 12345,
564 name: "evil.exe".into(),
565 host: "a.b.com".into(),
566 parent_process_id: Some(1234),
567 architecture: Some("x64".into()),
568 user: Some("bob".into()),
569 ..Default::default()
570 };
571 assert_eq!(
572 serde_json::from_str::<ProcessEntry>(&serde_json::to_string(&process).unwrap())
573 .unwrap(),
574 process
575 );
576
577 let cmd = CommandAction {
578 action: "add".into(),
579 cmd: "shell".into(),
580 };
581 assert_eq!(
582 serde_json::from_str::<CommandAction>(&serde_json::to_string(&cmd).unwrap()).unwrap(),
583 cmd
584 );
585
586 let keylog = KeylogEntry {
587 keystrokes: "password123".into(),
588 user: Some("alice".into()),
589 window_title: Some("Notepad".into()),
590 };
591 assert_eq!(
592 serde_json::from_str::<KeylogEntry>(&serde_json::to_string(&keylog).unwrap()).unwrap(),
593 keylog
594 );
595
596 let token = TokenEntry {
597 token_id: 18947,
598 host: "bob.com".into(),
599 user: "bob".into(),
600 process_id: Some(2345),
601 ..Default::default()
602 };
603 assert_eq!(
604 serde_json::from_str::<TokenEntry>(&serde_json::to_string(&token).unwrap()).unwrap(),
605 token
606 );
607
608 let cb_token = CallbackToken {
609 action: "add".into(),
610 host: "a.b.com".into(),
611 token_id: 12345,
612 };
613 assert_eq!(
614 serde_json::from_str::<CallbackToken>(&serde_json::to_string(&cb_token).unwrap())
615 .unwrap(),
616 cb_token
617 );
618
619 let removed = RemovedFileInfo {
620 host: "h".into(),
621 path: "/tmp/f".into(),
622 };
623 assert_eq!(
624 serde_json::from_str::<RemovedFileInfo>(&serde_json::to_string(&removed).unwrap())
625 .unwrap(),
626 removed
627 );
628
629 let chunk_download = TaskDownload {
630 total_chunks: None,
631 chunk_size: None,
632 filename: None,
633 full_path: None,
634 host: None,
635 is_screenshot: true,
636 file_id: Some(Uuid::from_u128(3)),
637 chunk_num: Some(1),
638 chunk_data: Some("cGFydA".to_string()),
639 };
640 assert_eq!(
641 serde_json::from_str::<TaskDownload>(&serde_json::to_string(&chunk_download).unwrap())
642 .unwrap(),
643 chunk_download
644 );
645
646 let full_response = TaskResponse {
648 task_id: uuid,
649 completed: Some(true),
650 status: Some("done".into()),
651 user_output: Some("ok".into()),
652 process_response: Some(serde_json::json!({"k": "v"})),
653 download: Some(task_download.clone()),
654 upload: Some(task_upload.clone()),
655 file_browser: Some(file_entry.clone()),
656 credentials: vec![credential.clone()],
657 artifacts: vec![artifact.clone()],
658 processes: vec![process.clone()],
659 commands: vec![cmd.clone()],
660 keylogs: vec![keylog.clone()],
661 tokens: vec![token.clone()],
662 callback_tokens: vec![cb_token.clone()],
663 removed_files: vec![removed.clone()],
664 alerts: vec![AlertMessage {
665 source: Some("a".into()),
666 level: Some("info".into()),
667 alert: Some("note".into()),
668 send_webhook: Some(false),
669 webhook_alert: Some(serde_json::json!({"x": 1})),
670 }],
671 edges: vec![EdgeMessage {
672 source: "src".into(),
673 destination: "dst".into(),
674 action: "add".into(),
675 c2_profile: "http".into(),
676 metadata: Some("meta".into()),
677 }],
678 socks: vec![SocksMessage {
679 server_id: 1,
680 exit: false,
681 data: Some("d".into()),
682 }],
683 rpfwd: vec![ReversePortForwardMessage {
684 server_id: 2,
685 exit: true,
686 data: None,
687 port: None,
688 }],
689 interactive: vec![InteractiveMessage {
690 task_id: uuid,
691 data: "stdin".into(),
692 message_type: 7,
693 }],
694 };
695 assert_eq!(
696 serde_json::from_str::<TaskResponse>(&serde_json::to_string(&full_response).unwrap())
697 .unwrap(),
698 full_response
699 );
700 }
701}