1use serde::{Deserialize, Serialize};
2
3#[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#[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#[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#[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#[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}