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
9pub 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}