Skip to main content

mythic/protocol/
get_tasking.rs

1//! Get-tasking message types — polling for new tasks, plus task/response/hooking
2//! types shared with [`super::post_response`].
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::{
7    string::{String, ToString},
8    vec::Vec,
9};
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() -> i32 {
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// ── Shared extras ─────────────────────────────────────────
37
38#[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/// Extras that an agent can attach to any request message.
55#[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
63/// Extras that Mythic can attach to any response message.
64pub type AgentResponseExtras = AgentExtras;
65
66// ── Get-tasking request / response ────────────────────────
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct ReqGetTasking {
70    pub action: String,
71    #[serde(default = "default_tasking_size")]
72    pub tasking_size: i32,
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: i32) -> 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: i32, 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    /// Build a `get_tasking` request carrying delegates, SOCKS, RPFWD,
99    /// interactive data, edges, alerts, and/or responses.
100    pub fn with_extras(tasking_size: i32, 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// ── TaskMessage ────────────────────────────────────────────
130
131#[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// ── TaskResponse and hooking feature types ─────────────────
140
141/// Task output sent by the agent.  All fields are optional except `task_id` so
142/// an agent can send back only what it needs (user output, file chunk, SOCKS
143/// data, etc.).
144#[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    // ── File transfer ──────────────────────────
157    #[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    // ── Hooking features ───────────────────────
163    #[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    // ── P2P / proxy ────────────────────────────
183    #[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("success".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// ── File transfer types ────────────────────────────────────
218
219#[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/// Agent-to-Mythic file chunk request (agent pulls a file from the server).
242#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
243pub struct TaskUpload {
244    pub chunk_size: u32,
245    pub file_id: Uuid,
246    /// 1-based chunk number the agent is requesting.
247    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// ── Hooking feature types ──────────────────────────────────
255
256/// File/directory entry for file browser results.
257#[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// ── Tests ──────────────────────────────────────────────────
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use std::{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_all = ReqGetTasking::new(-1);
409        let req_without = ReqGetTasking::with_delegate_tasks(3, false);
410
411        assert_eq!(req.action, ACTION_GET_TASKING);
412        assert_eq!(req.tasking_size, 9);
413        assert_eq!(req_all.tasking_size, -1);
414        assert!(req.get_delegate_tasks);
415        assert!(!req_without.get_delegate_tasks);
416    }
417
418    #[test]
419    fn extras_roundtrip() {
420        let extras = AgentMessageExtras::default();
421        assert_eq!(
422            serde_json::from_str::<AgentMessageExtras>(&serde_json::to_string(&extras).unwrap())
423                .unwrap(),
424            extras
425        );
426
427        let resp_extras = AgentResponseExtras::default();
428        assert_eq!(
429            serde_json::from_str::<AgentResponseExtras>(
430                &serde_json::to_string(&resp_extras).unwrap()
431            )
432            .unwrap(),
433            resp_extras
434        );
435    }
436
437    #[test]
438    fn resp_get_tasking_roundtrip() {
439        let uuid = Uuid::nil();
440        let resp = RespGetTasking {
441            action: ACTION_GET_TASKING.to_string(),
442            tasks: vec![TaskMessage {
443                command: "ls".to_string(),
444                parameters: "-la".to_string(),
445                timestamp: 1.0,
446                id: uuid,
447            }],
448            extras: AgentResponseExtras::default(),
449        };
450        assert_eq!(
451            serde_json::from_str::<RespGetTasking>(&serde_json::to_string(&resp).unwrap()).unwrap(),
452            resp
453        );
454    }
455
456    #[test]
457    fn task_response_default_is_empty() {
458        let resp = TaskResponse::default();
459        assert!(resp.user_output.is_none());
460        assert!(resp.download.is_none());
461        assert!(resp.upload.is_none());
462        assert!(resp.file_browser.is_none());
463        assert!(resp.credentials.is_empty());
464        assert!(resp.artifacts.is_empty());
465        assert!(resp.processes.is_empty());
466        assert!(resp.commands.is_empty());
467        assert!(resp.keylogs.is_empty());
468        assert!(resp.tokens.is_empty());
469        assert!(resp.callback_tokens.is_empty());
470        assert!(resp.removed_files.is_empty());
471    }
472
473    #[test]
474    fn task_models_roundtrip() {
475        let uuid = Uuid::nil();
476
477        let task_message = TaskMessage {
478            command: "ls".to_string(),
479            parameters: "-la".to_string(),
480            timestamp: 1.5,
481            id: uuid,
482        };
483        assert_eq!(
484            serde_json::from_str::<TaskMessage>(&serde_json::to_string(&task_message).unwrap())
485                .unwrap(),
486            task_message
487        );
488
489        let task_download = TaskDownload {
490            total_chunks: Some(2),
491            chunk_size: Some(64),
492            filename: Some("out.txt".to_string()),
493            full_path: Some("/tmp/out.txt".to_string()),
494            host: Some("host-a".to_string()),
495            is_screenshot: false,
496            file_id: None,
497            chunk_num: None,
498            chunk_data: None,
499        };
500        assert_eq!(
501            serde_json::from_str::<TaskDownload>(&serde_json::to_string(&task_download).unwrap())
502                .unwrap(),
503            task_download
504        );
505
506        let task_upload = TaskUpload {
507            chunk_size: 512000,
508            file_id: uuid,
509            chunk_num: 1,
510            full_path: Some("/tmp/target".into()),
511            host: Some("host-a".into()),
512        };
513        assert_eq!(
514            serde_json::from_str::<TaskUpload>(&serde_json::to_string(&task_upload).unwrap())
515                .unwrap(),
516            task_upload
517        );
518
519        let file_entry = FileBrowserEntry {
520            is_file: false,
521            name: "dir".into(),
522            host: Some("h".into()),
523            parent_path: Some("/".into()),
524            success: Some(true),
525            permissions: Some(serde_json::json!({"x": 1})),
526            files: vec![FileBrowserEntry {
527                is_file: true,
528                name: "f.txt".into(),
529                size: Some(100),
530                ..Default::default()
531            }],
532            ..Default::default()
533        };
534        assert_eq!(
535            serde_json::from_str::<FileBrowserEntry>(&serde_json::to_string(&file_entry).unwrap())
536                .unwrap(),
537            file_entry
538        );
539
540        let credential = Credential {
541            credential_type: "plaintext".into(),
542            credential: "pass123".into(),
543            account: "admin".into(),
544            realm: Some("DOMAIN".into()),
545            comment: None,
546        };
547        assert_eq!(
548            serde_json::from_str::<Credential>(&serde_json::to_string(&credential).unwrap())
549                .unwrap(),
550            credential
551        );
552
553        let artifact = Artifact {
554            base_artifact: "Process Create".into(),
555            artifact: "sh -c whoami".into(),
556            needs_cleanup: false,
557            resolved: false,
558        };
559        assert_eq!(
560            serde_json::from_str::<Artifact>(&serde_json::to_string(&artifact).unwrap()).unwrap(),
561            artifact
562        );
563
564        let process = ProcessEntry {
565            process_id: 12345,
566            name: "evil.exe".into(),
567            host: "a.b.com".into(),
568            parent_process_id: Some(1234),
569            architecture: Some("x64".into()),
570            user: Some("bob".into()),
571            ..Default::default()
572        };
573        assert_eq!(
574            serde_json::from_str::<ProcessEntry>(&serde_json::to_string(&process).unwrap())
575                .unwrap(),
576            process
577        );
578
579        let cmd = CommandAction {
580            action: "add".into(),
581            cmd: "shell".into(),
582        };
583        assert_eq!(
584            serde_json::from_str::<CommandAction>(&serde_json::to_string(&cmd).unwrap()).unwrap(),
585            cmd
586        );
587
588        let keylog = KeylogEntry {
589            keystrokes: "password123".into(),
590            user: Some("alice".into()),
591            window_title: Some("Notepad".into()),
592        };
593        assert_eq!(
594            serde_json::from_str::<KeylogEntry>(&serde_json::to_string(&keylog).unwrap()).unwrap(),
595            keylog
596        );
597
598        let token = TokenEntry {
599            token_id: 18947,
600            host: "bob.com".into(),
601            user: "bob".into(),
602            process_id: Some(2345),
603            ..Default::default()
604        };
605        assert_eq!(
606            serde_json::from_str::<TokenEntry>(&serde_json::to_string(&token).unwrap()).unwrap(),
607            token
608        );
609
610        let cb_token = CallbackToken {
611            action: "add".into(),
612            host: "a.b.com".into(),
613            token_id: 12345,
614        };
615        assert_eq!(
616            serde_json::from_str::<CallbackToken>(&serde_json::to_string(&cb_token).unwrap())
617                .unwrap(),
618            cb_token
619        );
620
621        let removed = RemovedFileInfo {
622            host: "h".into(),
623            path: "/tmp/f".into(),
624        };
625        assert_eq!(
626            serde_json::from_str::<RemovedFileInfo>(&serde_json::to_string(&removed).unwrap())
627                .unwrap(),
628            removed
629        );
630
631        let chunk_download = TaskDownload {
632            total_chunks: None,
633            chunk_size: None,
634            filename: None,
635            full_path: None,
636            host: None,
637            is_screenshot: true,
638            file_id: Some(Uuid::from_u128(3)),
639            chunk_num: Some(1),
640            chunk_data: Some("cGFydA".to_string()),
641        };
642        assert_eq!(
643            serde_json::from_str::<TaskDownload>(&serde_json::to_string(&chunk_download).unwrap())
644                .unwrap(),
645            chunk_download
646        );
647
648        // Full TaskResponse roundtrip with all hooking features populated
649        let full_response = TaskResponse {
650            task_id: uuid,
651            completed: Some(true),
652            status: Some("done".into()),
653            user_output: Some("ok".into()),
654            process_response: Some(serde_json::json!({"k": "v"})),
655            download: Some(task_download.clone()),
656            upload: Some(task_upload.clone()),
657            file_browser: Some(file_entry.clone()),
658            credentials: vec![credential.clone()],
659            artifacts: vec![artifact.clone()],
660            processes: vec![process.clone()],
661            commands: vec![cmd.clone()],
662            keylogs: vec![keylog.clone()],
663            tokens: vec![token.clone()],
664            callback_tokens: vec![cb_token.clone()],
665            removed_files: vec![removed.clone()],
666            alerts: vec![AlertMessage {
667                source: Some("a".into()),
668                level: Some("info".into()),
669                alert: Some("note".into()),
670                send_webhook: Some(false),
671                webhook_alert: Some(serde_json::json!({"x": 1})),
672            }],
673            edges: vec![EdgeMessage {
674                source: "src".into(),
675                destination: "dst".into(),
676                action: "add".into(),
677                c2_profile: "http".into(),
678                metadata: Some("meta".into()),
679            }],
680            socks: vec![SocksMessage {
681                server_id: 1,
682                exit: false,
683                data: Some("d".into()),
684            }],
685            rpfwd: vec![ReversePortForwardMessage {
686                server_id: 2,
687                exit: true,
688                data: None,
689                port: None,
690            }],
691            interactive: vec![InteractiveMessage {
692                task_id: uuid,
693                data: "stdin".into(),
694                message_type: 7,
695            }],
696        };
697        assert_eq!(
698            serde_json::from_str::<TaskResponse>(&serde_json::to_string(&full_response).unwrap())
699                .unwrap(),
700            full_response
701        );
702    }
703}