From f8bc7f84eacdc57bfba6077082448016058f416f Mon Sep 17 00:00:00 2001 From: Fabian Bees Date: Thu, 23 Jan 2025 13:47:15 +0100 Subject: [PATCH 1/4] iniitial work for also showing closed ports --- src/input.rs | 10 +++++- src/main.rs | 84 ++++++++++++++++++++++++++++++++++++++++++---- src/scanner/mod.rs | 39 ++++++++++++++++----- 3 files changed, 116 insertions(+), 17 deletions(-) diff --git a/src/input.rs b/src/input.rs index 2536e8b36..77868423b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -162,6 +162,10 @@ pub struct Opts { /// UDP scanning mode, finds UDP ports that send back responses #[arg(long)] pub udp: bool, + + /// Also show closed Ports + #[arg(long)] + pub closed: bool, } #[cfg(not(tarpaulin_include))] @@ -201,7 +205,7 @@ impl Opts { merge_required!( addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts, - command, udp + command, udp, closed ); } @@ -252,6 +256,7 @@ impl Default for Opts { exclude_ports: None, exclude_addresses: None, udp: false, + closed: false, } } } @@ -278,6 +283,7 @@ pub struct Config { exclude_ports: Option>, exclude_addresses: Option>, udp: Option, + closed: Option, } #[cfg(not(tarpaulin_include))] @@ -295,6 +301,7 @@ impl Config { /// scan_order = "Serial" /// exclude_ports = [8080, 9090, 80] /// udp = false + /// closed = false /// pub fn read(custom_config_path: Option) -> Self { let mut content = String::new(); @@ -353,6 +360,7 @@ mod tests { exclude_ports: None, exclude_addresses: None, udp: Some(false), + closed: Some(false), } } } diff --git a/src/main.rs b/src/main.rs index 52a998d74..a9632265d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,8 @@ extern crate log; /// Faster Nmap scanning with Rust /// If you're looking for the actual scanning, check out the module Scanner fn main() { + use rustscan::scanner::PortStatus; + #[cfg(not(unix))] let _ = ansi_term::enable_ansi_support(); @@ -93,6 +95,7 @@ fn main() { opts.accessible, opts.exclude_ports.unwrap_or_default(), opts.udp, + opts.closed, ); debug!("Scanner finished building: {:?}", scanner); @@ -101,17 +104,28 @@ fn main() { portscan_bench.end(); benchmarks.push(portscan_bench); - let mut ports_per_ip = HashMap::new(); + let mut open_ports_per_ip = HashMap::new(); + let mut closed_ports_per_ip = HashMap::new(); for socket in scan_result { - ports_per_ip - .entry(socket.ip()) - .or_insert_with(Vec::new) - .push(socket.port()); + match socket { + PortStatus::Open(socket) => { + open_ports_per_ip + .entry(socket.ip()) + .or_insert_with(Vec::new) + .push(socket.port()); + } + PortStatus::Closed(socket) => { + closed_ports_per_ip + .entry(socket.ip()) + .or_insert_with(Vec::new) + .push(socket.port()); + } + } } for ip in ips { - if ports_per_ip.contains_key(&ip) { + if open_ports_per_ip.contains_key(&ip) || closed_ports_per_ip.contains_key(&ip) { continue; } @@ -128,7 +142,63 @@ fn main() { } let mut script_bench = NamedTimer::start("Scripts"); - for (ip, ports) in &ports_per_ip { + for (ip, ports) in &open_ports_per_ip { + let vec_str_ports: Vec = ports.iter().map(ToString::to_string).collect(); + + // nmap port style is 80,443. Comma separated with no spaces. + let ports_str = vec_str_ports.join(","); + + // if option scripts is none, no script will be spawned + if opts.greppable || opts.scripts == ScriptsRequired::None { + println!("{} -> [{}]", &ip, ports_str); + continue; + } + detail!("Starting Script(s)", opts.greppable, opts.accessible); + + // Run all the scripts we found and parsed based on the script config file tags field. + for mut script_f in scripts_to_run.clone() { + // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command. + if !opts.command.is_empty() { + let user_extra_args = &opts.command.join(" "); + debug!("Extra args vec {:?}", user_extra_args); + if script_f.call_format.is_some() { + let mut call_f = script_f.call_format.unwrap(); + call_f.push(' '); + call_f.push_str(user_extra_args); + output!( + format!("Running script {:?} on ip {}\nDepending on the complexity of the script, results may take some time to appear.", call_f, &ip), + opts.greppable, + opts.accessible + ); + debug!("Call format {}", call_f); + script_f.call_format = Some(call_f); + } + } + + // Building the script with the arguments from the ScriptFile, and ip-ports. + let script = Script::build( + script_f.path, + *ip, + ports.clone(), + script_f.port, + script_f.ports_separator, + script_f.tags, + script_f.call_format, + ); + match script.run() { + Ok(script_result) => { + detail!(script_result.to_string(), opts.greppable, opts.accessible); + } + Err(e) => { + warning!(&format!("Error {e}"), opts.greppable, opts.accessible); + } + } + } + } + + // TODO: + println!("closed ports:"); + for (ip, ports) in &closed_ports_per_ip { let vec_str_ports: Vec = ports.iter().map(ToString::to_string).collect(); // nmap port style is 80,443. Comma separated with no spaces. diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 8d52fde7f..fedb0e94b 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -37,6 +37,13 @@ pub struct Scanner { accessible: bool, exclude_ports: Vec, udp: bool, + closed: bool, +} + +#[derive(Debug)] +pub enum PortStatus { + Open(SocketAddr), + Closed(SocketAddr), } // Allowing too many arguments for clippy. @@ -52,6 +59,7 @@ impl Scanner { accessible: bool, exclude_ports: Vec, udp: bool, + closed: bool, ) -> Self { Self { batch_size, @@ -63,13 +71,14 @@ impl Scanner { accessible, exclude_ports, udp, + closed, } } /// Runs scan_range with chunk sizes /// If you want to run RustScan normally, this is the entry point used /// Returns all open ports as `Vec` - pub async fn run(&self) -> Vec { + pub async fn run(&self) -> Vec { let ports: Vec = self .port_strategy .order() @@ -78,7 +87,7 @@ impl Scanner { .copied() .collect(); let mut socket_iterator: SocketIterator = SocketIterator::new(&self.ips, &ports); - let mut open_sockets: Vec = Vec::new(); + let mut found_sockets: Vec = Vec::new(); let mut ftrs = FuturesUnordered::new(); let mut errors: HashSet = HashSet::new(); let udp_map = get_parsed_data(); @@ -103,7 +112,7 @@ impl Scanner { } match result { - Ok(socket) => open_sockets.push(socket), + Ok(socket) => found_sockets.push(socket), Err(e) => { let error_string = e.to_string(); if errors.len() < self.ips.len() * 1000 { @@ -113,8 +122,8 @@ impl Scanner { } } debug!("Typical socket connection errors {:?}", errors); - debug!("Open Sockets found: {:?}", &open_sockets); - open_sockets + debug!("Open Sockets found: {:?}", &found_sockets); + found_sockets } /// Given a socket, scan it self.tries times. @@ -135,7 +144,7 @@ impl Scanner { &self, socket: SocketAddr, udp_map: BTreeMap, Vec>, - ) -> io::Result { + ) -> io::Result { if self.udp { return self.scan_udp_socket(socket, udp_map).await; } @@ -154,11 +163,23 @@ impl Scanner { self.fmt_ports(socket); debug!("Return Ok after {} tries", nr_try); - return Ok(socket); + return Ok(PortStatus::Open(socket)); } Err(e) => { let mut error_string = e.to_string(); + if e.kind() == io::ErrorKind::ConnectionRefused { + if !self.greppable { + if self.accessible { + println!("Closed {socket}"); + } else { + println!("Closed {}", socket.to_string().red()); + } + } + + return Ok(PortStatus::Closed(socket)); + } + assert!(!error_string.to_lowercase().contains("too many open files"), "Too many open files. Please reduce batch size. The default is 5000. Try -b 2500."); if nr_try == tries { @@ -176,7 +197,7 @@ impl Scanner { &self, socket: SocketAddr, udp_map: BTreeMap, Vec>, - ) -> io::Result { + ) -> io::Result { let mut payload: Vec = Vec::new(); for (key, value) in udp_map { if key.contains(&socket.port()) { @@ -187,7 +208,7 @@ impl Scanner { let tries = self.tries.get(); for _ in 1..=tries { match self.udp_scan(socket, &payload, self.timeout).await { - Ok(true) => return Ok(socket), + Ok(true) => return Ok(PortStatus::Open(socket)), Ok(false) => continue, Err(e) => return Err(e), } From 8eda7fc2aedf85e0ecd29013b7b46e623ec55f09 Mon Sep 17 00:00:00 2001 From: Fabian Bees Date: Wed, 22 Jan 2025 12:13:26 +0100 Subject: [PATCH 2/4] enable cli option --closed --- src/main.rs | 86 ++++++++++++---------------------------------- src/scanner/mod.rs | 2 +- 2 files changed, 23 insertions(+), 65 deletions(-) diff --git a/src/main.rs b/src/main.rs index a9632265d..6cc722a7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,9 +91,9 @@ fn main() { Duration::from_millis(opts.timeout.into()), opts.tries, opts.greppable, - PortStrategy::pick(&opts.range, opts.ports, opts.scan_order), + PortStrategy::pick(&opts.range, opts.clone().ports, opts.scan_order), opts.accessible, - opts.exclude_ports.unwrap_or_default(), + opts.exclude_ports.clone().unwrap_or_default(), opts.udp, opts.closed, ); @@ -142,63 +142,29 @@ fn main() { } let mut script_bench = NamedTimer::start("Scripts"); - for (ip, ports) in &open_ports_per_ip { - let vec_str_ports: Vec = ports.iter().map(ToString::to_string).collect(); - - // nmap port style is 80,443. Comma separated with no spaces. - let ports_str = vec_str_ports.join(","); - - // if option scripts is none, no script will be spawned - if opts.greppable || opts.scripts == ScriptsRequired::None { - println!("{} -> [{}]", &ip, ports_str); - continue; - } - detail!("Starting Script(s)", opts.greppable, opts.accessible); - - // Run all the scripts we found and parsed based on the script config file tags field. - for mut script_f in scripts_to_run.clone() { - // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command. - if !opts.command.is_empty() { - let user_extra_args = &opts.command.join(" "); - debug!("Extra args vec {:?}", user_extra_args); - if script_f.call_format.is_some() { - let mut call_f = script_f.call_format.unwrap(); - call_f.push(' '); - call_f.push_str(user_extra_args); - output!( - format!("Running script {:?} on ip {}\nDepending on the complexity of the script, results may take some time to appear.", call_f, &ip), - opts.greppable, - opts.accessible - ); - debug!("Call format {}", call_f); - script_f.call_format = Some(call_f); - } - } - // Building the script with the arguments from the ScriptFile, and ip-ports. - let script = Script::build( - script_f.path, - *ip, - ports.clone(), - script_f.port, - script_f.ports_separator, - script_f.tags, - script_f.call_format, - ); - match script.run() { - Ok(script_result) => { - detail!(script_result.to_string(), opts.greppable, opts.accessible); - } - Err(e) => { - warning!(&format!("Error {e}"), opts.greppable, opts.accessible); - } - } - } + print_summary(open_ports_per_ip, &scripts_to_run, &opts); + // We only print closed ports if the user requested it. + if opts.closed { + println!("closed ports:"); + print_summary(closed_ports_per_ip, &scripts_to_run, &opts); } - // TODO: - println!("closed ports:"); - for (ip, ports) in &closed_ports_per_ip { + // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan + script_bench.end(); + benchmarks.push(script_bench); + rustscan_bench.end(); + benchmarks.push(rustscan_bench); + debug!("Benchmarks raw {:?}", benchmarks); + info!("{}", benchmarks.summary()); +} + +fn print_summary( + ports_per_ip: HashMap>, + scripts_to_run: &Vec, + opts: &Opts, +) { + for (ip, ports) in &ports_per_ip { let vec_str_ports: Vec = ports.iter().map(ToString::to_string).collect(); // nmap port style is 80,443. Comma separated with no spaces. @@ -251,14 +217,6 @@ fn main() { } } } - - // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan - script_bench.end(); - benchmarks.push(script_bench); - rustscan_bench.end(); - benchmarks.push(rustscan_bench); - debug!("Benchmarks raw {:?}", benchmarks); - info!("{}", benchmarks.summary()); } /// Prints the opening title of RustScan diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index fedb0e94b..e0a49e9c4 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -169,7 +169,7 @@ impl Scanner { let mut error_string = e.to_string(); if e.kind() == io::ErrorKind::ConnectionRefused { - if !self.greppable { + if !self.greppable && self.closed { if self.accessible { println!("Closed {socket}"); } else { From 8929b63158b490664342671775264aa8d5736697 Mon Sep 17 00:00:00 2001 From: Fabian Bees Date: Wed, 22 Jan 2025 14:43:15 +0100 Subject: [PATCH 3/4] fix tests --- benches/benchmark_portscan.rs | 2 ++ src/lib.rs | 1 + src/main.rs | 3 +-- src/scanner/mod.rs | 9 +++++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/benches/benchmark_portscan.rs b/benches/benchmark_portscan.rs index 1206fa08e..89f082683 100644 --- a/benches/benchmark_portscan.rs +++ b/benches/benchmark_portscan.rs @@ -45,6 +45,7 @@ fn criterion_benchmark(c: &mut Criterion) { true, vec![], false, + false, ); c.bench_function("portscan tcp", |b| { @@ -61,6 +62,7 @@ fn criterion_benchmark(c: &mut Criterion) { true, vec![], true, + false, ); let mut udp_group = c.benchmark_group("portscan udp"); diff --git a/src/lib.rs b/src/lib.rs index e335de58a..b01c35a7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ //! true, // accessible, should the output be A11Y compliant? //! vec![9000], // What ports should RustScan exclude? //! false, // is this a UDP scan? +//! false, // Output closed ports? //! ); //! //! let scan_result = block_on(scanner.run()); diff --git a/src/main.rs b/src/main.rs index 6cc722a7d..26a95fe47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use std::string::ToString; use std::time::Duration; use rustscan::address::parse_addresses; +use rustscan::scanner::PortStatus; extern crate colorful; extern crate dirs; @@ -35,8 +36,6 @@ extern crate log; /// Faster Nmap scanning with Rust /// If you're looking for the actual scanning, check out the module Scanner fn main() { - use rustscan::scanner::PortStatus; - #[cfg(not(unix))] let _ = ansi_term::enable_ansi_support(); diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index e0a49e9c4..aff03d8f8 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -354,6 +354,7 @@ mod tests { true, vec![9000], false, + false, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -378,6 +379,7 @@ mod tests { true, vec![9000], false, + false, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -401,6 +403,7 @@ mod tests { true, vec![9000], false, + false, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -423,6 +426,7 @@ mod tests { true, vec![9000], false, + false, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -448,6 +452,7 @@ mod tests { true, vec![9000], false, + false, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -472,6 +477,7 @@ mod tests { true, vec![9000], true, + false, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -496,6 +502,7 @@ mod tests { true, vec![9000], true, + false, ); block_on(scanner.run()); // if the scan fails, it wouldn't be able to assert_eq! as it panicked! @@ -519,6 +526,7 @@ mod tests { true, vec![9000], true, + false, ); block_on(scanner.run()); assert_eq!(1, 1); @@ -541,6 +549,7 @@ mod tests { true, vec![9000], true, + false, ); block_on(scanner.run()); assert_eq!(1, 1); From 16f97cd0b97b5c0560323e747b61c7ebe919f2dc Mon Sep 17 00:00:00 2001 From: Fabian Bees Date: Thu, 23 Jan 2025 09:23:04 +0100 Subject: [PATCH 4/4] fix clippy warnings --- src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 26a95fe47..6c52d3d35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,11 +142,11 @@ fn main() { let mut script_bench = NamedTimer::start("Scripts"); - print_summary(open_ports_per_ip, &scripts_to_run, &opts); + print_summary(&open_ports_per_ip, &scripts_to_run, &opts); // We only print closed ports if the user requested it. if opts.closed { println!("closed ports:"); - print_summary(closed_ports_per_ip, &scripts_to_run, &opts); + print_summary(&closed_ports_per_ip, &scripts_to_run, &opts); } // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan @@ -159,11 +159,11 @@ fn main() { } fn print_summary( - ports_per_ip: HashMap>, - scripts_to_run: &Vec, + ports_per_ip: &HashMap>, + scripts_to_run: &[ScriptFile], opts: &Opts, ) { - for (ip, ports) in &ports_per_ip { + for (ip, ports) in ports_per_ip { let vec_str_ports: Vec = ports.iter().map(ToString::to_string).collect(); // nmap port style is 80,443. Comma separated with no spaces. @@ -177,7 +177,7 @@ fn print_summary( detail!("Starting Script(s)", opts.greppable, opts.accessible); // Run all the scripts we found and parsed based on the script config file tags field. - for mut script_f in scripts_to_run.clone() { + for mut script_f in scripts_to_run.iter().cloned() { // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command. if !opts.command.is_empty() { let user_extra_args = &opts.command.join(" ");