Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 2 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"] }
Expand All @@ -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"
Expand Down
67 changes: 63 additions & 4 deletions src/input.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<u16>`.
///
/// Example: `80,443,1000-1005` -> `[80, 443, 1000, 1001, 1002, 1003, 1004, 1005]`
pub fn parse_ports(input: &str) -> Result<Vec<u16>, 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::<u16>() {
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<PortRanges, String> {
let s = input.trim();
if s.is_empty() {
Expand Down Expand Up @@ -138,12 +177,12 @@ pub struct Opts {
#[arg(short, long, value_delimiter = ',')]
pub addresses: Vec<String>,

/// 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<Vec<u16>>,

/// 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<PortRanges>,

/// Whether to ignore the configuration file or not.
Expand Down Expand Up @@ -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"]);
Expand Down
22 changes: 20 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions src/port_strategy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ pub enum PortStrategy {

impl PortStrategy {
pub fn pick(range: &Option<PortRanges>, ports: Option<Vec<u16>>, 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();
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 23 additions & 26 deletions src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
));
}
Expand All @@ -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.
Expand All @@ -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));
}
}
};
Expand All @@ -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.
Expand All @@ -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));
}
}
};
Expand Down