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 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// ── 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: 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    /// Build a `get_tasking` request carrying delegates, SOCKS, RPFWD,
99    /// interactive data, edges, alerts, and/or responses.
100    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// ── 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("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// ── 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 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        // Full TaskResponse roundtrip with all hooking features populated
647        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}