Skip to main content

nessus_parser/
ping.rs

1//! Parse and normalize `plugin_output` text from ping-related Nessus finding.
2
3use crate::{MacAddress, error::FormatError};
4
5/// Structured interpretation of the `plugin_output` text for
6/// Nessus plugin `10180` ("Ping the remote host").
7#[derive(Debug, Clone, Copy)]
8pub enum PingOutcome {
9    /// The host was marked dead after no response to one or more ping methods.
10    HostDidntRespondToPingMethods { tcp: bool, icmp: bool },
11    /// The host replied to an ICMP echo packet.
12    IcmpEchoPacket,
13    /// The host is local-network reachable via ARP checks but did not answer ARP who-is.
14    HostLocalNetworkDidntRespondArpWhoIsQuery,
15    /// The host replied to an ARP who-is query with the given MAC address.
16    ArpWhoIsQuery(MacAddress),
17    /// The host replied with ICMP unreachable to a TCP SYN probe sent to `to`.
18    IcmpUnreachPacketInResponseToTcpSynPacket { to: u16 },
19    /// The host replied to a TCP SYN probe with `RST,ACK`.
20    RepliedTcpSynPacketWithRstAck { to: u16 },
21    /// The host replied to a TCP SYN probe with `SYN,ACK`.
22    RepliedTcpSynPacketWithSynAck { to: u16 },
23    /// The host replied to a TCP SYN probe with `FIN,ACK`.
24    RepliedTcpSynPacketWithFinAck { to: u16 },
25    /// The host emitted a TCP SYN packet.
26    EmittedTcpSynPacket { from: u16, to: u16 },
27    /// The host emitted a TCP ACK packet.
28    EmittedTcpAckPacket { from: u16, to: u16 },
29    /// The host emitted a TCP PUSH,ACK packet.
30    EmittedTcpPushAckPacket { from: u16, to: u16 },
31    /// The host replied with a generic ICMP unreachable packet.
32    IcmpUnreachPacket,
33    /// The host emitted a UDP packet.
34    EmittedUdpPacket { from: u16, to: u16 },
35    /// The target host is the same machine as the Nessus scanner.
36    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                // TODO: DRY
130                } 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}