Skip to main content

mythic/protocol/
post_response.rs

1//! Post-response message types — delivering task output back to Mythic.
2
3use alloc::{
4    string::{String, ToString},
5    vec::Vec,
6};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use super::{
11    ACTION_POST_RESPONSE,
12    get_tasking::{AgentMessageExtras, AgentResponseExtras, TaskResponse},
13};
14
15// ── Response receipt ───────────────────────────────────────
16
17#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
18pub struct ResponseReceipt {
19    pub task_id: Uuid,
20    pub status: String,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub file_id: Option<Uuid>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub error: Option<String>,
25}
26
27// ── Post-response request / response ──────────────────────
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30pub struct ReqPostResponse {
31    pub action: String,
32    #[serde(flatten)]
33    pub extras: AgentMessageExtras,
34}
35
36impl ReqPostResponse {
37    pub fn new(responses: Vec<TaskResponse>) -> Self {
38        Self {
39            action: ACTION_POST_RESPONSE.to_string(),
40            extras: AgentMessageExtras {
41                responses,
42                shared: super::get_tasking::AgentExtras::default(),
43            },
44        }
45    }
46
47    /// Build a `post_response` from a pre-built [`AgentMessageExtras`]
48    /// (responses + delegates, SOCKS, RPFWD, edges, etc.).
49    pub fn from_extras(extras: AgentMessageExtras) -> Self {
50        Self {
51            action: ACTION_POST_RESPONSE.to_string(),
52            extras,
53        }
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
58pub struct RespPostResponse {
59    pub action: String,
60    #[serde(default)]
61    pub responses: Vec<ResponseReceipt>,
62    #[serde(flatten)]
63    pub extras: AgentResponseExtras,
64}
65
66impl RespPostResponse {
67    pub fn new(responses: Vec<ResponseReceipt>) -> Self {
68        Self {
69            action: ACTION_POST_RESPONSE.to_string(),
70            responses,
71            extras: AgentResponseExtras::default(),
72        }
73    }
74}
75
76// ── Tests ──────────────────────────────────────────────────
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use alloc::vec;
82
83    #[test]
84    fn post_response_wraps_responses() {
85        let task_id = Uuid::nil();
86        let req = ReqPostResponse::new(vec![TaskResponse::completed(task_id, "ok")]);
87
88        assert_eq!(req.action, ACTION_POST_RESPONSE);
89        assert_eq!(req.extras.responses.len(), 1);
90        assert_eq!(req.extras.responses[0].task_id, task_id);
91        assert_eq!(req.extras.responses[0].status.as_deref(), Some("completed"));
92    }
93
94    #[test]
95    fn receipt_roundtrip() {
96        let uuid = Uuid::nil();
97        let receipt = ResponseReceipt {
98            task_id: uuid,
99            status: "sent".to_string(),
100            file_id: Some(Uuid::from_u128(1)),
101            error: Some("none".to_string()),
102        };
103        assert_eq!(
104            serde_json::from_str::<ResponseReceipt>(&serde_json::to_string(&receipt).unwrap())
105                .unwrap(),
106            receipt
107        );
108    }
109
110    #[test]
111    fn resp_post_response_roundtrip() {
112        let uuid = Uuid::nil();
113        let next_uuid = Uuid::from_u128(1);
114        let resp = RespPostResponse {
115            action: ACTION_POST_RESPONSE.to_string(),
116            responses: vec![ResponseReceipt {
117                task_id: uuid,
118                status: "sent".to_string(),
119                file_id: Some(next_uuid),
120                error: None,
121            }],
122            extras: AgentResponseExtras::default(),
123        };
124        assert_eq!(
125            serde_json::from_str::<RespPostResponse>(&serde_json::to_string(&resp).unwrap())
126                .unwrap(),
127            resp
128        );
129    }
130}