1use crate::{MacAddress, error::FormatError};
4
5#[derive(Debug, Clone, Copy)]
8pub enum PingOutcome {
9 HostDidntRespondToPingMethods { tcp: bool, icmp: bool },
11 IcmpEchoPacket,
13 HostLocalNetworkDidntRespondArpWhoIsQuery,
15 ArpWhoIsQuery(MacAddress),
17 IcmpUnreachPacketInResponseToTcpSynPacket { to: u16 },
19 RepliedTcpSynPacketWithRstAck { to: u16 },
21 RepliedTcpSynPacketWithSynAck { to: u16 },
23 RepliedTcpSynPacketWithFinAck { to: u16 },
25 EmittedTcpSynPacket { from: u16, to: u16 },
27 EmittedTcpAckPacket { from: u16, to: u16 },
29 EmittedTcpPushAckPacket { from: u16, to: u16 },
31 IcmpUnreachPacket,
33 EmittedUdpPacket { from: u16, to: u16 },
35 LocalScanner,
37}
38
39impl PingOutcome {
40 #[allow(clippy::too_many_lines)]
41 pub(crate) fn from_plugin_output(plugin_output: &str) -> Result<Self, FormatError> {
42 const LOCAL_SCANNER: &str = "The remote host is up\nThe host is the local scanner.";
43 const ICMP_ECHO_PACKET: &str =
44 "The remote host is up\nThe remote host replied to an ICMP echo packet";
45 const ICMP_UNREACH_PACKET: &str =
46 "The remote host is up\nThe remote host replied with an ICMP unreach packet.";
47 const ARP_WHO_IS_QUERY_PREFIX: &str =
48 "The remote host is up\nThe host replied to an ARP who-is query.\nHardware address : ";
49 const REPLIED_TCP_SYN_PACKET_PREFIX: &str =
50 "The remote host is up\nThe remote host replied to a TCP SYN packet sent to port ";
51 const WITH_RST_ACK_SUFFIX: &str = " with a RST,ACK packet";
52 const WITH_SYN_ACK_SUFFIX: &str = " with a SYN,ACK packet";
53 const WITH_FIN_ACK_SUFFIX: &str = " with a FIN,ACK packet";
54 const ICMP_UNREACH_PACKET_RESPONSE_TO_TCP_SYN_PACKET_PREFIX: &str = "The remote host is up\nThe remote host replied with an ICMP unreach packet sent in response to a TCP SYN packet sent to port ";
55 const EMITTED_UDP_PACKET_PREFIX: &str =
56 "The remote host is up\nThe remote host emited a UDP packet from port ";
57 const EMITTED_PACKET_MIDDLE: &str = "going to port ";
58 const EMITTED_TCP_SYN_PACKET_PREFIX: &str =
59 "The remote host is up\nThe remote host emitted a TCP SYN packet from port ";
60 const EMITTED_TCP_ACK_PACKET_PREFIX: &str =
61 "The remote host is up\nThe remote host emitted a TCP ACK packet from port ";
62 const EMITTED_TCP_PUSH_ACK_PACKET_PREFIX: &str =
63 "The remote host is up\nThe remote host emitted a TCP PUSH,ACK packet from port ";
64 const DEAD_HOST_NEEDLE: &str = ") is considered as dead - not scanning\nThe remote host (";
65 const FAILED_TO_REPLY_ARP_SUFFIX: &str =
66 ") is on the local network and failed to reply to an ARP who-is query.";
67 const DIDNT_RESPOND_TO_PING_METHODS_NEEDLE: &str =
68 ") did not respond to the following ping methods :\n";
69
70 match plugin_output {
71 ICMP_ECHO_PACKET => Ok(Self::IcmpEchoPacket),
72 LOCAL_SCANNER => Ok(Self::LocalScanner),
73 ICMP_UNREACH_PACKET => Ok(Self::IcmpUnreachPacket),
74 text => {
75 if text.contains(DEAD_HOST_NEEDLE) {
76 if text.ends_with(FAILED_TO_REPLY_ARP_SUFFIX) {
77 Ok(Self::HostLocalNetworkDidntRespondArpWhoIsQuery)
78 } else if let Some((_, ping_methods)) =
79 text.split_once(DIDNT_RESPOND_TO_PING_METHODS_NEEDLE)
80 {
81 let mut tcp = false;
82 let mut icmp = false;
83 for ping_method in ping_methods.trim_end().split('\n') {
84 match ping_method {
85 "- TCP ping" => tcp = true,
86 "- ICMP ping" => icmp = true,
87 _ => {
88 return Err(FormatError::UnexpectedPingMethod(
89 plugin_output.into(),
90 ));
91 }
92 }
93 }
94 if !tcp && !icmp {
95 Err(FormatError::UnexpectedPingFormat(plugin_output.into()))
96 } else {
97 Ok(Self::HostDidntRespondToPingMethods { tcp, icmp })
98 }
99 } else {
100 Err(FormatError::UnexpectedDeadHostReason(plugin_output.into()))
101 }
102 } else if let Some(mac) = text.strip_prefix(ARP_WHO_IS_QUERY_PREFIX) {
103 Ok(Self::ArpWhoIsQuery(mac.parse()?))
104 } else if let Some(port) =
105 text.strip_prefix(ICMP_UNREACH_PACKET_RESPONSE_TO_TCP_SYN_PACKET_PREFIX)
106 {
107 Ok(Self::IcmpUnreachPacketInResponseToTcpSynPacket { to: port.parse()? })
108 } else if let Some(port_and_suffix) =
109 text.strip_prefix(REPLIED_TCP_SYN_PACKET_PREFIX)
110 {
111 if let Some(port) = port_and_suffix.strip_suffix(WITH_RST_ACK_SUFFIX) {
112 Ok(Self::RepliedTcpSynPacketWithRstAck { to: port.parse()? })
113 } else if let Some(port) = port_and_suffix.strip_suffix(WITH_SYN_ACK_SUFFIX) {
114 Ok(Self::RepliedTcpSynPacketWithSynAck { to: port.parse()? })
115 } else if let Some(port) = port_and_suffix.strip_suffix(WITH_FIN_ACK_SUFFIX) {
116 Ok(Self::RepliedTcpSynPacketWithFinAck { to: port.parse()? })
117 } else {
118 Err(FormatError::UnexpectedPingTcpResponse(plugin_output.into()))
119 }
120 } else if let Some(from_port_and_rest) =
121 text.strip_prefix(EMITTED_TCP_SYN_PACKET_PREFIX)
122 && let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
123 && let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
124 {
125 Ok(Self::EmittedTcpSynPacket {
126 from: from_port.parse()?,
127 to: to_port.parse()?,
128 })
129 } else if let Some(from_port_and_rest) =
131 text.strip_prefix(EMITTED_TCP_ACK_PACKET_PREFIX)
132 && let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
133 && let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
134 {
135 Ok(Self::EmittedTcpAckPacket {
136 from: from_port.parse()?,
137 to: to_port.parse()?,
138 })
139 } else if let Some(from_port_and_rest) =
140 text.strip_prefix(EMITTED_TCP_PUSH_ACK_PACKET_PREFIX)
141 && let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
142 && let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
143 {
144 Ok(Self::EmittedTcpPushAckPacket {
145 from: from_port.parse()?,
146 to: to_port.parse()?,
147 })
148 } else if let Some(from_port_and_rest) =
149 text.strip_prefix(EMITTED_UDP_PACKET_PREFIX)
150 && let Some((from_port, rest)) = from_port_and_rest.split_once(' ')
151 && let Some(to_port) = rest.strip_prefix(EMITTED_PACKET_MIDDLE)
152 {
153 Ok(Self::EmittedUdpPacket {
154 from: from_port.parse()?,
155 to: to_port.parse()?,
156 })
157 } else {
158 Err(FormatError::UnexpectedPingFormat(plugin_output.into()))
159 }
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use crate::error::FormatError;
168
169 use super::PingOutcome;
170
171 #[test]
172 fn parses_common_ping_outcomes() {
173 let icmp_echo = "The remote host is up\nThe remote host replied to an ICMP echo packet";
174 assert!(matches!(
175 PingOutcome::from_plugin_output(icmp_echo).expect("must parse"),
176 PingOutcome::IcmpEchoPacket
177 ));
178
179 let local = "The remote host is up\nThe host is the local scanner.";
180 assert!(matches!(
181 PingOutcome::from_plugin_output(local).expect("must parse"),
182 PingOutcome::LocalScanner
183 ));
184
185 let arp = "The remote host is up\nThe host replied to an ARP who-is query.\nHardware address : 0a:1b:2c:3d:4e:5f";
186 assert!(matches!(
187 PingOutcome::from_plugin_output(arp).expect("must parse"),
188 PingOutcome::ArpWhoIsQuery(_)
189 ));
190 }
191
192 #[test]
193 fn parses_tcp_and_udp_based_outcomes() {
194 let syn_ack = "The remote host is up\nThe remote host replied to a TCP SYN packet sent to port 443 with a SYN,ACK packet";
195 assert!(matches!(
196 PingOutcome::from_plugin_output(syn_ack).expect("must parse"),
197 PingOutcome::RepliedTcpSynPacketWithSynAck { to: 443 }
198 ));
199
200 let emitted_udp = "The remote host is up\nThe remote host emited a UDP packet from port 68 going to port 67";
201 assert!(matches!(
202 PingOutcome::from_plugin_output(emitted_udp).expect("must parse"),
203 PingOutcome::EmittedUdpPacket { from: 68, to: 67 }
204 ));
205 }
206
207 #[test]
208 fn parses_dead_host_ping_methods() {
209 let text = "The remote host (1.2.3.4) is considered as dead - not scanning\nThe remote host (1.2.3.4) did not respond to the following ping methods :\n- TCP ping\n- ICMP ping";
210 assert!(matches!(
211 PingOutcome::from_plugin_output(text).expect("must parse"),
212 PingOutcome::HostDidntRespondToPingMethods {
213 tcp: true,
214 icmp: true
215 }
216 ));
217 }
218
219 #[test]
220 fn rejects_unexpected_ping_method_lines() {
221 let text = "The remote host (1.2.3.4) is considered as dead - not scanning\nThe remote host (1.2.3.4) did not respond to the following ping methods :\n- UDP ping";
222 let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
223 assert!(matches!(err, FormatError::UnexpectedPingMethod(_)));
224 }
225
226 #[test]
227 fn rejects_unknown_dead_host_reason() {
228 let text = "The remote host (1.2.3.4) is considered as dead - not scanning\nThe remote host (1.2.3.4) failed for an unknown reason.";
229 let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
230 assert!(matches!(err, FormatError::UnexpectedDeadHostReason(_)));
231 }
232
233 #[test]
234 fn rejects_unexpected_tcp_reply_suffix() {
235 let text = "The remote host is up\nThe remote host replied to a TCP SYN packet sent to port 443 with a MAYBE packet";
236 let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
237 assert!(matches!(err, FormatError::UnexpectedPingTcpResponse(_)));
238 }
239
240 #[test]
241 fn rejects_unrecognized_ping_format() {
242 let text = "completely unexpected";
243 let err = PingOutcome::from_plugin_output(text).expect_err("must fail");
244 assert!(matches!(err, FormatError::UnexpectedPingFormat(_)));
245 }
246}