Skip to main content

nmap/
gnmap.rs

1use crate::{
2    Address, AddressType, ExtraPorts, ExtraReasons, Host, HostName, HostNameType, HostNames,
3    HostState, NmapRun, Port, PortProtocol, PortState, PortStateDetails, Ports, Service,
4    ServiceMethod, Status,
5};
6use std::collections::HashMap;
7use std::io::{self, BufRead};
8
9/// Parses Nmap greppable (-oG) output format into a structured format
10///
11/// # Examples
12///
13/// ```
14/// use std::io::Cursor;
15/// use nmap::gnmap::parse_gnmap;
16///
17/// let gnmap_data = r#"# Nmap 7.92 scan initiated Mon Mar 10 10:11:59 2025 as: nmap -sS -p 80 example.com
18/// Host: 192.168.1.1 ()    Status: Up
19/// Host: 192.168.1.1 ()    Ports: 80/open/tcp//http//nginx/
20/// # Nmap done at Mon Mar 10 10:12:01 2025 -- 1 IP address (1 host up) scanned in 2.49 seconds"#;
21///
22/// let cursor = Cursor::new(gnmap_data);
23/// let result = parse_gnmap(cursor);
24/// assert!(result.is_ok());
25///
26/// let nmap_run = result.unwrap();
27/// assert_eq!(nmap_run.hosts.len(), 1);
28///
29/// let host = &nmap_run.hosts[0];
30/// assert_eq!(host.addresses[0].addr, "192.168.1.1");
31/// assert_eq!(host.status.state, nmap::HostState::Up);
32///
33/// if let Some(ports) = &host.ports {
34///     if let Some(port_list) = &ports.ports {
35///         assert_eq!(port_list.len(), 1);
36///         assert_eq!(port_list[0].port_id, 80);
37///         assert_eq!(port_list[0].state.state, nmap::PortState::Open);
38///         if let Some(service) = &port_list[0].service {
39///             assert_eq!(service.name, "http");
40///             assert_eq!(service.product, Some("nginx".to_string()));
41///         }
42///     }
43/// }
44/// ```
45pub fn parse_gnmap<R: io::Read>(reader: R) -> Result<NmapRun, String> {
46    let reader = io::BufReader::new(reader);
47    let mut lines = reader.lines();
48
49    let header = lines
50        .next()
51        .ok_or_else(|| "Empty gnmap file".to_string())?
52        .map_err(|e| format!("Failed to read header: {}", e))?;
53
54    let args = header
55        .split("as: ")
56        .nth(1)
57        .ok_or_else(|| "Invalid header format".to_string())?
58        .to_string();
59
60    let mut hosts = Vec::new();
61    let mut host_map: HashMap<String, usize> = HashMap::new();
62
63    while let Some(Ok(line)) = lines.next() {
64        if line.starts_with("# Nmap done") {
65            continue;
66        }
67
68        if line.contains("Status: Up") {
69            let ip = extract_ip(&line)?;
70            let hostname = extract_hostname(&line);
71
72            if !host_map.contains_key(&ip) {
73                let index = hosts.len();
74                let mut host = Host {
75                    status: Status {
76                        state: HostState::Up,
77                        reason: "user-set".to_string(),
78                        reason_ttl: "0".to_string(),
79                    },
80                    addresses: vec![Address {
81                        addr: ip.clone(),
82                        addrtype: AddressType::IPv4,
83                        vendor: None,
84                    }],
85                    hostnames: None,
86                    ports: None,
87                    start_time: None,
88                    end_time: None,
89                };
90
91                if let Some(name) = hostname {
92                    host.add_hostname(name, HostNameType::User);
93                }
94
95                hosts.push(host);
96                host_map.insert(ip, index);
97            }
98        } else if line.contains("Ports:") {
99            let ip = extract_ip(&line)?;
100
101            let host_index = *host_map
102                .get(&ip)
103                .ok_or_else(|| format!("Found ports for unknown host: {}", ip))?;
104
105            let ports_section = line
106                .split("Ports: ")
107                .nth(1)
108                .ok_or_else(|| "Invalid ports line format".to_string())?;
109
110            let mut port_list = Vec::new();
111            let mut ignored_count = 0;
112
113            for port_entry in ports_section.split("\t").next().unwrap().split(", ") {
114                if port_entry.contains("Ignored State:") {
115                    if let Some(count) = port_entry
116                        .split("(")
117                        .nth(1)
118                        .and_then(|s| s.split(")").next())
119                    {
120                        if let Ok(count) = count.parse::<u32>() {
121                            ignored_count = count;
122                        }
123                    }
124                    continue;
125                }
126
127                let parts: Vec<&str> = port_entry.split("/").collect();
128                if parts.len() >= 7 {
129                    let port_id = parts[0].parse::<u16>().unwrap_or(0);
130                    let state = match parts[1] {
131                        "open" => PortState::Open,
132                        "closed" => PortState::Closed,
133                        "filtered" => PortState::Filtered,
134                        "unfiltered" => PortState::Unfiltered,
135                        "open|filtered" => PortState::OpenFiltered,
136                        "closed|filtered" => PortState::ClosedFiltered,
137                        _ => PortState::Open,
138                    };
139                    let protocol = match parts[2] {
140                        "tcp" => PortProtocol::Tcp,
141                        "udp" => PortProtocol::Udp,
142                        "sctp" => PortProtocol::Sctp,
143                        "ip" => PortProtocol::Ip,
144                        _ => PortProtocol::Tcp,
145                    };
146
147                    let service_name = parts[4].to_string();
148                    let service_details = parts[6].to_string();
149
150                    let mut service = Service {
151                        name: service_name,
152                        product: None,
153                        version: None,
154                        extra_info: None,
155                        method: ServiceMethod::Table,
156                        confidence: 3,
157                        os_type: None,
158                        device_type: None,
159                        tunnel: None,
160                        cpes: None,
161                    };
162
163                    if !service_details.is_empty() {
164                        let details = service_details.trim_end_matches('/');
165                        let first_word = details.split_whitespace().next().unwrap_or("");
166                        service.product = Some(first_word.to_string());
167
168                        if details.contains(' ') {
169                            let mut parts = details.split_whitespace();
170                            let _ = parts.next();
171                            let rest: String = parts.collect::<Vec<_>>().join(" ");
172
173                            if let Some(ver_idx) = rest.find(|c: char| c.is_numeric()) {
174                                let version_end = rest[ver_idx..]
175                                    .find(|c: char| !c.is_numeric() && c != '.' && c != '-')
176                                    .map(|i| ver_idx + i)
177                                    .unwrap_or(rest.len());
178                                service.version = Some(rest[ver_idx..version_end].to_string());
179
180                                if let Some(extra_start) = rest.find('(') {
181                                    if let Some(extra_end) = rest.rfind(')') {
182                                        service.extra_info = Some(
183                                            rest[extra_start + 1..extra_end].trim().to_string(),
184                                        );
185                                    }
186                                }
187                            }
188                        }
189
190                        service.method = ServiceMethod::Probed;
191                        service.confidence = 10;
192                    }
193
194                    let port = Port {
195                        protocol,
196                        port_id: port_id as u32,
197                        state: PortStateDetails {
198                            state,
199                            reason: "syn-ack".to_string(),
200                            reason_ttl: "0".to_string(),
201                            reason_ip: None,
202                        },
203                        service: Some(service),
204                        scripts: None,
205                    };
206
207                    port_list.push(port);
208                }
209            }
210
211            if !port_list.is_empty() || ignored_count > 0 {
212                let mut extra_ports = None;
213                if ignored_count > 0 {
214                    extra_ports = Some(vec![ExtraPorts {
215                        state: PortState::Closed,
216                        count: ignored_count,
217                        extrareasons: Some(vec![ExtraReasons {
218                            reason: "conn-refused".to_string(),
219                            count: ignored_count,
220                            protocol: None,
221                            ports: None,
222                        }]),
223                    }]);
224                }
225
226                hosts[host_index].ports = Some(Ports {
227                    ports: Some(port_list),
228                    extraports: extra_ports,
229                });
230            }
231        }
232    }
233
234    let nmap_run = NmapRun {
235        scanner: "nmap".to_string(),
236        args,
237        start: None,
238        start_str: None,
239        version: "7.92".to_string(),
240        xml_output_version: "1.05".to_string(),
241        scan_info: None,
242        verbose: None,
243        debugging: None,
244        hosts,
245        run_stats: None,
246    };
247
248    Ok(nmap_run)
249}
250
251fn extract_ip(line: &str) -> Result<String, String> {
252    line.split_whitespace()
253        .nth(1)
254        .ok_or_else(|| "IP address not found".to_string())
255        .map(|s| s.to_string())
256}
257
258fn extract_hostname(line: &str) -> Option<String> {
259    if line.contains("(") && line.contains(")") {
260        let start = line.find("(")? + 1;
261        let end = line.find(")")?;
262        if start < end {
263            let hostname = &line[start..end];
264            if !hostname.is_empty() {
265                return Some(hostname.to_string());
266            }
267        }
268    }
269    None
270}
271
272trait HostExt {
273    fn add_hostname(&mut self, name: String, hostname_type: HostNameType);
274}
275
276impl HostExt for Host {
277    fn add_hostname(&mut self, name: String, hostname_type: HostNameType) {
278        if name.is_empty() {
279            return;
280        }
281
282        let hostname = HostName {
283            name,
284            hostname_type,
285        };
286
287        match &mut self.hostnames {
288            Some(hostnames) => {
289                if let Some(ref mut hostname_vec) = hostnames.hostnames {
290                    hostname_vec.push(hostname);
291                } else {
292                    hostnames.hostnames = Some(vec![hostname]);
293                }
294            }
295            None => {
296                self.hostnames = Some(HostNames {
297                    hostnames: Some(vec![hostname]),
298                });
299            }
300        }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use std::io::Cursor;
308
309    #[test]
310    fn test_parse_gnmap() {
311        let gnmap_data = r#"# Nmap 7.92 scan initiated Mon Mar 10 10:11:59 2025 as: nmap -sS -Pn -A -O -T4 -oA local 192.168.0.0/24
312Host: 1.1.1.1 ()	Status: Up
313Host: 1.1.1.1 ()	Ports: 1443/open/tcp//ssl|upnp//apache/
314Host: 2.2.2.2 ()	Status: Up
315Host: 2.2.2.2 ()	Ports: 80/open/tcp//http//nginx/, 443/open/tcp//ssl|http//nginx/, 8080/open/tcp//http//nginx/
316Host: 3.3.3.3 (three.local)	Status: Up
317Host: 3.3.3.3 (three.local)	Ports: 	Ignored State: closed (1000)
318# Nmap done at Mon Mar 10 10:17:20 2025 -- 256 IP addresses (15 hosts up) scanned in 320.49 seconds"#;
319
320        let cursor = Cursor::new(gnmap_data);
321        let result = parse_gnmap(cursor);
322
323        assert!(result.is_ok(), "Failed to parse gnmap: {:?}", result.err());
324
325        let nmap_run = result.unwrap();
326        assert_eq!(nmap_run.hosts.len(), 3, "Expected 3 hosts");
327
328        let host_two = nmap_run
329            .hosts
330            .iter()
331            .find(|h| h.addresses.iter().any(|a| a.addr == "2.2.2.2"));
332
333        assert!(host_two.is_some(), "Host 2.2.2.2 not found");
334        let host = host_two.unwrap();
335
336        assert_eq!(host.status.state, HostState::Up);
337
338        if let Some(ports) = &host.ports {
339            if let Some(port_list) = &ports.ports {
340                let http_port = port_list.iter().find(|p| p.port_id == 80);
341                assert!(http_port.is_some(), "HTTP port not found");
342
343                let https_port = port_list.iter().find(|p| p.port_id == 443);
344                assert!(https_port.is_some(), "HTTPS port not found");
345
346                if let Some(port) = http_port {
347                    assert_eq!(port.state.state, PortState::Open);
348                    if let Some(service) = &port.service {
349                        assert_eq!(service.name, "http");
350                        assert_eq!(service.product, Some("nginx".to_string()));
351                        assert_eq!(service.version, None);
352                    }
353                }
354
355                let apache_host = nmap_run
356                    .hosts
357                    .iter()
358                    .find(|h| h.addresses.iter().any(|a| a.addr == "1.1.1.1"))
359                    .expect("Apache host not found");
360
361                if let Some(apache_ports) = &apache_host.ports {
362                    if let Some(port_list) = &apache_ports.ports {
363                        let upnp_port = port_list.iter().find(|p| p.port_id == 1443);
364                        assert!(upnp_port.is_some(), "UPnP port not found");
365
366                        if let Some(port) = upnp_port {
367                            if let Some(service) = &port.service {
368                                assert_eq!(service.name, "ssl|upnp");
369                                assert_eq!(service.product, Some("apache".to_string()));
370                            } else {
371                                panic!("No service info for UPnP port");
372                            }
373                        }
374                    }
375                }
376            } else {
377                panic!("No ports found for host 2.2.2.2");
378            }
379        } else {
380            panic!("No ports found for host 2.2.2.2");
381        }
382
383        let host_with_hostname = nmap_run
384            .hosts
385            .iter()
386            .find(|h| h.addresses.iter().any(|a| a.addr == "3.3.3.3"));
387
388        if let Some(host) = host_with_hostname {
389            assert!(host.hostnames.is_some(), "No hostnames for 3.3.3.3");
390            if let Some(hostnames) = &host.hostnames {
391                if let Some(hostname_vec) = &hostnames.hostnames {
392                    assert!(!hostname_vec.is_empty(), "Empty hostnames list");
393                    assert_eq!(hostname_vec[0].name, "three.local");
394                } else {
395                    panic!("No hostname vector found");
396                }
397            }
398        } else {
399            panic!("Host 3.3.3.3 not found");
400        }
401    }
402}