Skip to main content

sshfwd_common/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// A listening port discovered on the remote host.
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct ListeningPort {
6    pub protocol: Protocol,
7    pub local_addr: String,
8    pub port: u16,
9    pub process: Option<ProcessInfo>,
10}
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
13#[serde(rename_all = "lowercase")]
14pub enum Protocol {
15    Tcp,
16    Tcp6,
17}
18
19/// Information about the process owning a listening socket.
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct ProcessInfo {
22    pub pid: u32,
23    pub name: String,
24    pub cmdline: String,
25    pub uid: u32,
26}
27
28/// A single scan snapshot from the agent.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct ScanResult {
31    pub agent_version: String,
32    pub hostname: String,
33    pub username: String,
34    pub is_root: bool,
35    pub ports: Vec<ListeningPort>,
36    pub warnings: Vec<String>,
37    pub scan_index: u64,
38}
39
40/// Top-level response envelope from the agent (one per JSON line).
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(tag = "status", rename_all = "lowercase")]
43pub enum AgentResponse {
44    Ok(ScanResult),
45    Error(AgentError),
46}
47
48/// An error reported by the agent.
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct AgentError {
51    pub kind: AgentErrorKind,
52    pub message: String,
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum AgentErrorKind {
58    ScanFailed,
59    PermissionDenied,
60    Unsupported,
61}
62
63impl std::fmt::Display for AgentErrorKind {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            Self::ScanFailed => write!(f, "scan_failed"),
67            Self::PermissionDenied => write!(f, "permission_denied"),
68            Self::Unsupported => write!(f, "unsupported"),
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn sample_scan_result() -> ScanResult {
78        ScanResult {
79            agent_version: "0.1.0".to_string(),
80            hostname: "server1".to_string(),
81            username: "deploy".to_string(),
82            is_root: false,
83            ports: vec![
84                ListeningPort {
85                    protocol: Protocol::Tcp,
86                    local_addr: "127.0.0.1".to_string(),
87                    port: 5432,
88                    process: Some(ProcessInfo {
89                        pid: 1234,
90                        name: "postgres".to_string(),
91                        cmdline: "/usr/lib/postgresql/15/bin/postgres".to_string(),
92                        uid: 108,
93                    }),
94                },
95                ListeningPort {
96                    protocol: Protocol::Tcp6,
97                    local_addr: "::".to_string(),
98                    port: 8080,
99                    process: None,
100                },
101            ],
102            warnings: vec!["permission denied reading /proc/999/fd".to_string()],
103            scan_index: 42,
104        }
105    }
106
107    #[test]
108    fn scan_result_round_trip() {
109        let result = sample_scan_result();
110        let json = serde_json::to_string(&result).unwrap();
111        let deserialized: ScanResult = serde_json::from_str(&json).unwrap();
112        assert_eq!(result, deserialized);
113    }
114
115    #[test]
116    fn agent_response_ok_round_trip() {
117        let response = AgentResponse::Ok(sample_scan_result());
118        let json = serde_json::to_string(&response).unwrap();
119        let deserialized: AgentResponse = serde_json::from_str(&json).unwrap();
120        assert_eq!(response, deserialized);
121    }
122
123    #[test]
124    fn agent_response_error_round_trip() {
125        let response = AgentResponse::Error(AgentError {
126            kind: AgentErrorKind::ScanFailed,
127            message: "failed to read /proc/net/tcp".to_string(),
128        });
129        let json = serde_json::to_string(&response).unwrap();
130        let deserialized: AgentResponse = serde_json::from_str(&json).unwrap();
131        assert_eq!(response, deserialized);
132    }
133
134    #[test]
135    fn agent_response_ok_json_structure() {
136        let response = AgentResponse::Ok(ScanResult {
137            agent_version: "0.1.0".to_string(),
138            hostname: "h".to_string(),
139            username: "u".to_string(),
140            is_root: true,
141            ports: vec![],
142            warnings: vec![],
143            scan_index: 0,
144        });
145        let json = serde_json::to_string(&response).unwrap();
146        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
147        assert_eq!(value["status"], "ok");
148        assert_eq!(value["agent_version"], "0.1.0");
149    }
150
151    #[test]
152    fn agent_response_error_json_structure() {
153        let response = AgentResponse::Error(AgentError {
154            kind: AgentErrorKind::PermissionDenied,
155            message: "access denied".to_string(),
156        });
157        let json = serde_json::to_string(&response).unwrap();
158        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
159        assert_eq!(value["status"], "error");
160        assert_eq!(value["kind"], "permission_denied");
161    }
162
163    #[test]
164    fn protocol_serialization() {
165        assert_eq!(serde_json::to_string(&Protocol::Tcp).unwrap(), "\"tcp\"");
166        assert_eq!(serde_json::to_string(&Protocol::Tcp6).unwrap(), "\"tcp6\"");
167    }
168
169    #[test]
170    fn listening_port_without_process() {
171        let port = ListeningPort {
172            protocol: Protocol::Tcp,
173            local_addr: "0.0.0.0".to_string(),
174            port: 80,
175            process: None,
176        };
177        let json = serde_json::to_string(&port).unwrap();
178        let deserialized: ListeningPort = serde_json::from_str(&json).unwrap();
179        assert_eq!(port, deserialized);
180        assert!(json.contains("\"process\":null"));
181    }
182}