use crate::Result;
use std::fs;
#[derive(Debug, Clone)]
pub struct NmapTarget {
pub hostname: String,
pub ip: String,
pub port: u16,
pub protocol: String,
pub state: String,
}
pub struct NmapParser;
impl NmapParser {
pub fn parse_file(path: &str) -> Result<Vec<NmapTarget>> {
let content = fs::read_to_string(path)?;
Self::parse_content(&content)
}
pub fn parse_content(content: &str) -> Result<Vec<NmapTarget>> {
let mut targets = Vec::new();
for line in content.lines() {
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
if line.starts_with("Host:")
&& let Some(parsed_targets) = Self::parse_host_line(line)
{
targets.extend(parsed_targets);
}
}
Ok(targets)
}
fn parse_host_line(line: &str) -> Option<Vec<NmapTarget>> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return None;
}
let ip = parts[1].to_string();
let hostname = if parts.len() > 2 && parts[2].starts_with('(') {
parts[2].trim_matches(|c| c == '(' || c == ')').to_string()
} else {
ip.clone()
};
let ports_section = line.split("Ports:").nth(1)?;
let mut targets = Vec::new();
for port_entry in ports_section.split(',') {
let port_parts: Vec<&str> = port_entry.trim().split('/').collect();
if port_parts.len() >= 3 {
let port = port_parts[0].parse::<u16>().ok()?;
let state = port_parts[1].to_string();
let protocol = port_parts[2].to_string();
if state == "open" {
targets.push(NmapTarget {
hostname: hostname.clone(),
ip: ip.clone(),
port,
protocol,
state,
});
}
}
}
Some(targets)
}
pub fn to_target_strings(targets: &[NmapTarget]) -> Vec<String> {
targets
.iter()
.filter(|t| t.protocol == "tcp") .map(|t| format!("{}:{}", t.hostname, t.port))
.collect()
}
pub fn filter_by_ports(targets: &[NmapTarget], ports: &[u16]) -> Vec<NmapTarget> {
targets
.iter()
.filter(|t| ports.contains(&t.port))
.cloned()
.collect()
}
pub fn get_tls_ports(targets: &[NmapTarget]) -> Vec<NmapTarget> {
let tls_ports = vec![
443, 465, 587, 993, 995, 3306, 5432, 636, 8443, ];
Self::filter_by_ports(targets, &tls_ports)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_content() {
let content = r#"# Nmap 7.80 scan
Host: 192.168.1.1 (example.com) Status: Up
Host: 192.168.1.1 (example.com) Ports: 443/open/tcp//https/// Ignored State: closed (999)
"#;
let targets = NmapParser::parse_content(content).expect("test assertion should succeed");
assert!(!targets.is_empty());
}
#[test]
fn test_to_target_strings() {
let targets = vec![NmapTarget {
hostname: "example.com".to_string(),
ip: "192.168.1.1".to_string(),
port: 443,
protocol: "tcp".to_string(),
state: "open".to_string(),
}];
let strings = NmapParser::to_target_strings(&targets);
assert_eq!(strings[0], "example.com:443");
}
#[test]
fn test_filter_by_ports() {
let targets = vec![
NmapTarget {
hostname: "example.com".to_string(),
ip: "192.168.1.1".to_string(),
port: 443,
protocol: "tcp".to_string(),
state: "open".to_string(),
},
NmapTarget {
hostname: "example.com".to_string(),
ip: "192.168.1.1".to_string(),
port: 80,
protocol: "tcp".to_string(),
state: "open".to_string(),
},
];
let filtered = NmapParser::filter_by_ports(&targets, &[443]);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].port, 443);
}
}