1use std::collections::HashMap;
14
15pub mod discovery;
16pub mod security;
17pub mod service;
18pub mod set;
19
20pub use discovery::{Discovery, ScanResponse};
21pub use security::{CertificateInfo, Security};
22pub use service::Service;
23pub use set::{PortSet, PortSetParseError};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum Protocol {
28 Tcp,
29 Udp,
30 Sctp,
31}
32
33#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
40pub enum PortState {
41 ClosedFiltered,
43
44 Filtered,
46
47 Unfiltered,
49
50 Closed,
52
53 OpenFiltered,
55
56 Open,
58}
59
60#[derive(Debug, Clone, PartialEq)]
65pub enum ScriptOutput {
66 String(String),
67 Integer(i64),
68 Float(f64),
69 Boolean(bool),
70 List(Vec<ScriptOutput>),
71 Map(HashMap<String, ScriptOutput>),
72}
73
74#[derive(Debug, Clone, PartialEq)]
79pub struct Port {
80 number: u16,
82
83 protocol: Protocol,
85
86 state: PortState,
88
89 service: Option<Service>,
91
92 security: Option<Security>,
94
95 discovery: Option<Discovery>,
97
98 scripts: Option<HashMap<String, ScriptOutput>>,
101}
102
103impl Port {
104 pub fn new(number: u16, protocol: Protocol, state: PortState) -> Self {
106 Self {
107 number,
108 protocol,
109 state,
110 service: None,
111 security: None,
112 discovery: None,
113 scripts: None,
114 }
115 }
116
117 pub fn number(&self) -> u16 {
119 self.number
120 }
121
122 pub fn protocol(&self) -> Protocol {
124 self.protocol
125 }
126
127 pub fn state(&self) -> PortState {
129 self.state
130 }
131
132 pub fn set_state(&mut self, state: PortState) {
134 self.state = state;
135 }
136
137 pub fn service(&self) -> Option<&Service> {
139 self.service.as_ref()
140 }
141
142 pub fn service_name(&self) -> Option<&str> {
146 self.service.as_ref().map(|s| s.name())
147 }
148
149 pub fn set_service(&mut self, service: Service) {
151 self.service = Some(service);
152 }
153
154 pub fn security(&self) -> Option<&Security> {
156 self.security.as_ref()
157 }
158
159 pub fn set_security(&mut self, security: Security) {
161 self.security = Some(security);
162 }
163
164 pub fn discovery(&self) -> Option<&Discovery> {
166 self.discovery.as_ref()
167 }
168
169 pub fn scripts(&self) -> Option<&HashMap<String, ScriptOutput>> {
171 self.scripts.as_ref()
172 }
173
174 pub fn with_service(mut self, service: Service) -> Self {
176 self.service = Some(service);
177 self
178 }
179
180 pub fn with_security(mut self, security: Security) -> Self {
182 self.security = Some(security);
183 self
184 }
185
186 pub fn with_discovery(mut self, discovery: Discovery) -> Self {
188 self.discovery = Some(discovery);
189 self
190 }
191
192 pub fn add_script(mut self, key: impl Into<String>, output: ScriptOutput) -> Self {
194 let scripts = self.scripts.get_or_insert_with(HashMap::new);
195 scripts.insert(key.into(), output);
196 self
197 }
198
199 pub fn merge(&mut self, mut other: Port) {
204 self.state = std::cmp::max(self.state, other.state);
206
207 if let Some(other_service) = other.service {
209 if let Some(ref mut self_service) = self.service {
210 self_service.merge(other_service);
211 } else {
212 self.service = Some(other_service);
213 }
214 }
215
216 if let Some(other_security) = other.security {
218 if let Some(ref mut self_security) = self.security {
219 self_security.merge(other_security);
220 } else {
221 self.security = Some(other_security);
222 }
223 }
224
225 if other.state >= self.state && other.discovery.is_some() {
228 self.discovery = other.discovery;
229 }
230
231 if let Some(other_scripts) = other.scripts.take() {
233 let self_scripts = self.scripts.get_or_insert_with(HashMap::new);
234 for (key, value) in other_scripts {
235 self_scripts.insert(key, value);
236 }
237 }
238 }
239}
240
241#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn port_state_ordering_upgrades_correctly() {
256 let mut p1 = Port::new(80, Protocol::Tcp, PortState::Filtered);
258 p1.merge(Port::new(80, Protocol::Tcp, PortState::Open));
259 assert_eq!(p1.state(), PortState::Open);
260
261 let mut p2 = Port::new(53, Protocol::Udp, PortState::OpenFiltered);
263 p2.merge(Port::new(53, Protocol::Udp, PortState::Open));
264 assert_eq!(p2.state(), PortState::Open);
265
266 let mut p3 = Port::new(443, Protocol::Tcp, PortState::Unfiltered);
268 p3.merge(Port::new(443, Protocol::Tcp, PortState::Closed));
269 assert_eq!(p3.state(), PortState::Closed);
270 }
271
272 #[test]
273 fn structured_scripts_merge_correctly() {
274 let mut port = Port::new(80, Protocol::Tcp, PortState::Open)
275 .add_script("http-title", ScriptOutput::String("Index".into()));
276
277 let mut ssh_keys = HashMap::new();
279 ssh_keys.insert("rsa".into(), ScriptOutput::Integer(2048));
280 ssh_keys.insert("ed25519".into(), ScriptOutput::Integer(256));
281
282 let other = Port::new(80, Protocol::Tcp, PortState::Open)
283 .add_script("ssh-hostkey", ScriptOutput::Map(ssh_keys));
284
285 port.merge(other);
286
287 let scripts = port.scripts.as_ref().unwrap();
288 assert_eq!(scripts.len(), 2);
289 assert!(matches!(
290 scripts.get("http-title"),
291 Some(ScriptOutput::String(_))
292 ));
293 assert!(matches!(
294 scripts.get("ssh-hostkey"),
295 Some(ScriptOutput::Map(_))
296 ));
297 }
298
299 #[test]
300 fn discovery_telemetry_upgrades_on_better_state() {
301 let disc_filtered = Discovery::new(ScanResponse::NoResponse);
302 let mut p_filtered =
303 Port::new(22, Protocol::Tcp, PortState::Filtered).with_discovery(disc_filtered.clone());
304
305 let disc_open = Discovery::new(ScanResponse::TcpSynAck);
306 let p_open =
307 Port::new(22, Protocol::Tcp, PortState::Open).with_discovery(disc_open.clone());
308
309 p_filtered.merge(p_open);
311
312 assert_eq!(p_filtered.state(), PortState::Open);
313 assert_eq!(
314 p_filtered.discovery().unwrap().reason(),
315 &ScanResponse::TcpSynAck
316 );
317 }
318}