diff --git a/Cargo.lock b/Cargo.lock index a7204a129..ff340ea8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,15 +38,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "1.0.0" @@ -273,15 +264,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "colored" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "colorful" version = "0.3.2" @@ -1420,12 +1402,9 @@ dependencies = [ name = "rustscan" version = "2.4.1" dependencies = [ - "ansi_term", - "anstream", "anyhow", "cidr-utils", "clap", - "colored", "colorful", "criterion", "dirs", @@ -1441,7 +1420,6 @@ dependencies = [ "rand 0.10.3", "rlimit", "serde", - "serde_derive", "text_placeholder", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index c45530658..9e9709352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ exclude = [ [dependencies] clap = { version = "4.6.1", features = ["derive", "wrap_help"] } -colored = "3.1.1" # Async runtime for the scanner. hickory-resolver already depends on tokio, so # this adds no crates; only the features the scanner itself uses are listed. tokio = { version = "1.44.2", features = ["rt", "net", "time"] } @@ -27,15 +26,12 @@ futures = { version = "0.3", default-features = false, features = ["std"] } rlimit = "0.11.0" log = "0.4.32" env_logger = "0.11.10" -anstream = "=1.0.0" dirs = "7.0.0" gcd = "2.0.1" rand = "0.10.1" colorful = "0.3.2" -ansi_term = "0.12.1" toml = "1.1.2" -serde = "1.0.124" -serde_derive = "1.0.116" +serde = { version = "1.0", features = ["derive"] } cidr-utils = "0.7.1" itertools = "0.15.0" hickory-resolver = { version = "0.24.3", features = ["dns-over-rustls"] } @@ -47,7 +43,7 @@ once_cell = "1.21.4" libc = "0.2" [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Networking_WinSock"] } +windows-sys = { version = "0.59", features = ["Win32_Networking_WinSock", "Win32_System_Console"] } [dev-dependencies] parameterized = "2.0.0" diff --git a/src/input.rs b/src/input.rs index e7d948800..b0bb27b43 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,7 @@ //! Provides a means to read, parse and hold configuration options for scans. use clap::parser::ValueSource; use clap::{CommandFactory, FromArgMatches, Parser, ValueEnum}; -use serde_derive::Deserialize; +use serde::Deserialize; use std::collections::HashSet; use std::ffi::OsString; use std::fs; @@ -98,6 +98,45 @@ fn parse_range(input: &str) -> Option<(u16, u16)> { /// Parse a comma-separated list of `start-end` ranges into `PortRanges`. /// /// Errors with a helpful message identifying the bad token. +#[cfg(not(tarpaulin_include))] +/// Parse a comma-separated list of ports or `start-end` port ranges into a flat `Vec`. +/// +/// Example: `80,443,1000-1005` -> `[80, 443, 1000, 1001, 1002, 1003, 1004, 1005]` +pub fn parse_ports(input: &str) -> Result, String> { + let s = input.trim(); + if s.is_empty() { + return Err( + "empty input: expected one or more comma-separated ports or port ranges".into(), + ); + } + + let mut ports = Vec::new(); + for token in s.split(',') { + let t = token.trim(); + if t.is_empty() { + continue; + } + if let Some((start, end)) = parse_range(t) { + for p in start..=end { + ports.push(p); + } + } else if let Ok(port) = t.parse::() { + if port == 0 { + return Err("port number 0 is invalid — expected 1 <= port <= 65535".into()); + } + ports.push(port); + } else { + return Err(format!( + "invalid port or range token `{t}` — expected port (e.g. 80) or range (e.g. 1-1000)" + )); + } + } + if ports.is_empty() { + return Err("no valid ports parsed".into()); + } + Ok(ports) +} + fn parse_ranges(input: &str) -> Result { let s = input.trim(); if s.is_empty() { @@ -138,12 +177,12 @@ pub struct Opts { #[arg(short, long, value_delimiter = ',')] pub addresses: Vec, - /// A list of comma separated ports to be scanned. Example: 80,443,8080. - #[arg(short, long, value_delimiter = ',')] + /// A list of comma separated ports or port ranges to be scanned. Example: 80,443,1000-2000. + #[arg(short, long, value_parser = parse_ports)] pub ports: Option>, /// Ranges of ports as comma-separated start-end pairs. Example: 1-500,1000-2500,4000-7000 - #[arg(short, long, conflicts_with = "ports", value_parser = parse_ranges)] + #[arg(short, long, value_parser = parse_ranges)] pub range: Option, /// Whether to ignore the configuration file or not. @@ -819,6 +858,26 @@ mod tests { } } + #[test] + fn parses_mixed_ports_and_ranges() { + let ports = super::parse_ports("80,443,8000-8003,9000").expect("should parse"); + assert_eq!(ports, vec![80, 443, 8000, 8001, 8002, 8003, 9000]); + } + + #[test] + fn parses_ports_with_whitespace() { + let ports = super::parse_ports(" 80 , 443 , 1000-1002 ").expect("should parse"); + assert_eq!(ports, vec![80, 443, 1000, 1001, 1002]); + } + + #[test] + fn rejects_invalid_port_tokens() { + assert!(super::parse_ports("80,abc").is_err()); + assert!(super::parse_ports("0").is_err()); + assert!(super::parse_ports("100-50").is_err()); + assert!(super::parse_ports("").is_err()); + } + #[test] fn parses_interval_in_milliseconds() { let opts = Opts::parse_from(["rustscan", "-a", "127.0.0.1", "--interval", "250"]); diff --git a/src/main.rs b/src/main.rs index d0c8add84..94612e1b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,9 +35,27 @@ extern crate log; #[allow(clippy::too_many_lines)] /// Faster Nmap scanning with Rust /// If you're looking for the actual scanning, check out the module Scanner +#[cfg(windows)] +fn enable_ansi_support() { + use windows_sys::Win32::System::Console::{ + GetConsoleMode, GetStdHandle, SetConsoleMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING, + STD_OUTPUT_HANDLE, + }; + unsafe { + let handle = GetStdHandle(STD_OUTPUT_HANDLE); + let mut mode = 0; + if GetConsoleMode(handle, &mut mode) != 0 { + SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + } +} + +#[cfg(not(windows))] +fn enable_ansi_support() {} + +#[allow(clippy::too_many_lines)] fn main() { - #[cfg(not(unix))] - let _ = ansi_term::enable_ansi_support(); + enable_ansi_support(); env_logger::init(); let mut benchmarks = Benchmark::init(); diff --git a/src/port_strategy/mod.rs b/src/port_strategy/mod.rs index a873583f6..e3ee732c2 100644 --- a/src/port_strategy/mod.rs +++ b/src/port_strategy/mod.rs @@ -18,6 +18,20 @@ pub enum PortStrategy { impl PortStrategy { pub fn pick(range: &Option, ports: Option>, order: ScanOrder) -> Self { + let ports = match (ports, range) { + (Some(mut p), Some(r)) => { + for (start, end) in &r.0 { + for port in *start..=*end { + p.push(port); + } + } + let mut seen = std::collections::HashSet::new(); + p.retain(|port| seen.insert(*port)); + Some(p) + } + (Some(p), None) => Some(p), + (None, _) => None, + }; match order { ScanOrder::Serial if ports.is_none() => { let port_ranges = range.as_ref().unwrap(); @@ -137,6 +151,14 @@ mod tests { assert_eq!(vec![80, 443], result); } + #[test] + fn picks_both_ports_and_range() { + let range = Some(PortRanges(vec![(8080, 8082)])); + let ports = Some(vec![80, 443, 8081]); + let strategy = PortStrategy::pick(&range, ports, ScanOrder::Serial); + assert_eq!(strategy.order(), vec![80, 443, 8081, 8080, 8082]); + } + #[test] fn random_strategy_with_ports() { let strategy = PortStrategy::pick(&None, Some((1..10).collect()), ScanOrder::Random); diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 78e7ff090..00a1a7278 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -10,7 +10,7 @@ use socket_iterator::SocketIterator; mod errors; use errors::{diagnostic_error, is_descriptor_exhaustion, ScanErrors}; -use colored::Colorize; +use colorful::Colorful; use futures::stream::{FuturesUnordered, StreamExt}; use std::collections::BTreeMap; use std::future::poll_fn; @@ -530,7 +530,7 @@ impl Scanner { if self.accessible { println_safe(format_args!("Open {socket}")); } else { - println_safe(format_args!("Open {}", socket.to_string().purple())); + println_safe(format_args!("Open {}", socket.to_string().magenta())); } } } diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index c22d63a85..93e4f5aab 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -75,7 +75,7 @@ use crate::input::ScriptsRequired; use anyhow::{anyhow, Result}; use log::debug; -use serde_derive::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use std::fs::{self, File}; use std::io::{self, prelude::*}; use std::net::IpAddr; diff --git a/src/tui.rs b/src/tui.rs index f38fd87bb..ecd1e1a91 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2,6 +2,23 @@ use std::io::Write; +use colorful::Colorful; + +#[inline] +pub fn icon_warning() -> colorful::core::color_string::CString { + "[!]".red().bold() +} + +#[inline] +pub fn icon_detail() -> colorful::core::color_string::CString { + "[~]".blue().bold() +} + +#[inline] +pub fn icon_output() -> colorful::core::color_string::CString { + "[>]".color(colorful::RGB::new(0, 255, 9)).bold() +} + /// Prints a single line to stdout without ever panicking. /// /// `println!` panics when writing to stdout fails, which is fatal under the @@ -32,11 +49,7 @@ pub fn println_safe(args: std::fmt::Arguments<'_>) { #[macro_export] macro_rules! warning { ($name:expr) => { - $crate::tui::println_safe(format_args!( - "{} {}", - ansi_term::Colour::Red.bold().paint("[!]"), - $name - )); + $crate::tui::println_safe(format_args!("{} {}", $crate::tui::icon_warning(), $name)); }; ($name:expr, $greppable:expr, $accessible:expr) => { // if not greppable then print, otherwise no else statement so do not print. @@ -47,7 +60,7 @@ macro_rules! warning { } else { $crate::tui::println_safe(format_args!( "{} {}", - ansi_term::Colour::Red.bold().paint("[!]"), + $crate::tui::icon_warning(), $name )); } @@ -58,11 +71,7 @@ macro_rules! warning { #[macro_export] macro_rules! detail { ($name:expr) => { - $crate::tui::println_safe(format_args!( - "{} {}", - ansi_term::Colour::Blue.bold().paint("[~]"), - $name - )); + $crate::tui::println_safe(format_args!("{} {}", $crate::tui::icon_detail(), $name)); }; ($name:expr, $greppable:expr, $accessible:expr) => { // if not greppable then print, otherwise no else statement so do not print. @@ -71,11 +80,7 @@ macro_rules! detail { // Don't print the ascii art $crate::tui::println_safe(format_args!("{}", $name)); } else { - $crate::tui::println_safe(format_args!( - "{} {}", - ansi_term::Colour::Blue.bold().paint("[~]"), - $name - )); + $crate::tui::println_safe(format_args!("{} {}", $crate::tui::icon_detail(), $name)); } } }; @@ -84,11 +89,7 @@ macro_rules! detail { #[macro_export] macro_rules! output { ($name:expr) => { - $crate::tui::println_safe(format_args!( - "{} {}", - ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"), - $name - )); + $crate::tui::println_safe(format_args!("{} {}", $crate::tui::icon_output(), $name)); }; ($name:expr, $greppable:expr, $accessible:expr) => { // if not greppable then print, otherwise no else statement so do not print. @@ -97,11 +98,7 @@ macro_rules! output { // Don't print the ascii art $crate::tui::println_safe(format_args!("{}", $name)); } else { - $crate::tui::println_safe(format_args!( - "{} {}", - ansi_term::Colour::RGB(0, 255, 9).bold().paint("[>]"), - $name - )); + $crate::tui::println_safe(format_args!("{} {}", $crate::tui::icon_output(), $name)); } } };