use crate::scanner::PortResult;
use colored::Colorize;
use serde::Serialize;
use std::fs::File;
use std::io::Write;
use std::time::Duration;
#[derive(Debug, Serialize)]
pub struct ScanResult {
pub host: String,
pub results: Vec<PortResult>,
pub scan_duration: Option<Duration>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl ScanResult {
pub fn new(host: String, results: Vec<PortResult>, scan_duration: Option<Duration>) -> Self {
ScanResult {
host,
results,
scan_duration,
timestamp: chrono::Utc::now(),
}
}
pub fn to_json_file(&self, path: &str) -> Result<(), std::io::Error> {
let json = serde_json::to_string_pretty(self)?;
let mut file = File::create(path)?;
file.write_all(json.as_bytes())?;
println!("Scan results saved to {}", path);
Ok(())
}
pub fn to_csv_file(&self, path: &str) -> Result<(), std::io::Error> {
let mut wtr = csv::Writer::from_path(path)?;
wtr.write_record(&[
"Port",
"Status",
"Service",
"Protocol",
"Latency (ms)",
"Scan Time",
])?;
for result in &self.results {
let is_open_filtered = result
.protocol_info
.as_ref()
.map_or(false, |p| p.contains("open|filtered"));
let status = if result.is_open {
"Open"
} else if is_open_filtered {
"Open|Filtered"
} else {
"Closed"
};
let unknown = "Unknown".to_string();
let service = result.service.as_ref().unwrap_or(&unknown);
let none_str = "None".to_string();
let protocol = result.protocol_info.as_ref().unwrap_or(&none_str);
let latency = match result.latency {
Some(duration) => format!("{:.2}", duration.as_secs_f64() * 1000.0),
None => "-".to_string(),
};
let scan_time = result.scan_time.to_rfc3339();
wtr.write_record(&[
result.port.to_string(),
status.to_string(),
service.to_string(),
protocol.to_string(),
latency,
scan_time,
])?;
}
wtr.flush()?;
println!("Scan results saved to {}", path);
Ok(())
}
pub fn print(&self) {
println!("{}", "\nScan Results".bold());
println!("{}: {}", "Target".bold(), self.host);
if let Some(duration) = self.scan_duration {
println!(
"{}: {:.2} seconds",
"Duration".bold(),
duration.as_secs_f64()
);
}
println!("{}: {}", "Timestamp".bold(), self.timestamp.to_rfc3339());
println!("{}: {}", "Ports Scanned".bold(), self.results.len());
let open_ports = self.results.iter().filter(|r| r.is_open).count();
let open_filtered_ports = self
.results
.iter()
.filter(|r| {
!r.is_open
&& r.protocol_info
.as_ref()
.map_or(false, |p| p.contains("open|filtered"))
})
.count();
println!("{}: {}", "Open Ports".bold(), open_ports);
if open_filtered_ports > 0 {
println!("{}: {}", "Open|Filtered Ports".bold(), open_filtered_ports);
}
println!();
println!(
"{:6} {:10} {:20} {:20} {:10}",
"PORT".bold(),
"STATE".bold(),
"SERVICE".bold(),
"PROTOCOL".bold(),
"LATENCY".bold()
);
println!("{}", "-".repeat(70));
for result in &self.results {
let is_open_filtered = result
.protocol_info
.as_ref()
.map_or(false, |p| p.contains("open|filtered"));
if !result.is_open && !is_open_filtered {
continue;
}
let state = if result.is_open {
"open".green()
} else if is_open_filtered {
"open|filtered".yellow()
} else {
"closed".red()
};
let unknown = "unknown".to_string();
let service = result.service.as_ref().unwrap_or(&unknown);
let dash = "-".to_string();
let protocol = result.protocol_info.as_ref().unwrap_or(&dash);
let latency = match result.latency {
Some(duration) => format!("{:.2}ms", duration.as_secs_f64() * 1000.0),
None => "-".to_string(),
};
println!(
"{:6} {:10} {:20} {:20} {:10}",
result.port.to_string(),
state,
service,
protocol,
latency
);
}
println!();
}
pub fn print_verbose(&self) {
println!("{}", "\nDetailed Scan Results".bold());
println!("{}: {}", "Target".bold(), self.host);
if let Some(duration) = self.scan_duration {
println!(
"{}: {:.2} seconds",
"Duration".bold(),
duration.as_secs_f64()
);
}
println!("{}: {}", "Timestamp".bold(), self.timestamp.to_rfc3339());
println!("{}: {}", "Ports Scanned".bold(), self.results.len());
let open_ports = self.results.iter().filter(|r| r.is_open).count();
let open_filtered_ports = self
.results
.iter()
.filter(|r| {
!r.is_open
&& r.protocol_info
.as_ref()
.map_or(false, |p| p.contains("open|filtered"))
})
.count();
let closed_ports = self.results.len() - open_ports - open_filtered_ports;
println!("{}: {}", "Open Ports".bold(), open_ports);
if open_filtered_ports > 0 {
println!("{}: {}", "Open|Filtered Ports".bold(), open_filtered_ports);
}
println!("{}: {}", "Closed Ports".bold(), closed_ports);
println!();
println!(
"{:6} {:10} {:20} {:20} {:10}",
"PORT".bold(),
"STATE".bold(),
"SERVICE".bold(),
"PROTOCOL".bold(),
"LATENCY".bold()
);
println!("{}", "-".repeat(70));
for result in &self.results {
let is_open_filtered = result
.protocol_info
.as_ref()
.map_or(false, |p| p.contains("open|filtered"));
let state = if result.is_open {
"open".green()
} else if is_open_filtered {
"open|filtered".yellow()
} else {
"closed".red()
};
let unknown = "unknown".to_string();
let service = result.service.as_ref().unwrap_or(&unknown);
let dash = "-".to_string();
let protocol = result.protocol_info.as_ref().unwrap_or(&dash);
let latency = match result.latency {
Some(duration) => format!("{:.2}ms", duration.as_secs_f64() * 1000.0),
None => "-".to_string(),
};
println!(
"{:6} {:10} {:20} {:20} {:10}",
result.port.to_string(),
state,
service,
protocol,
latency
);
}
println!();
}
}