From 47302266428af447618370a5893bedf9571caf91 Mon Sep 17 00:00:00 2001 From: kill-ux Date: Sun, 1 Feb 2026 16:09:30 +0100 Subject: [PATCH 1/4] add bin folder --- src/{main.rs => bin/pentestkit.rs} | 0 todo.todo | 13 ------------- wordlist.txt | 8 -------- {input => wordlist}/wordlist.txt | 10 +++++++++- 4 files changed, 9 insertions(+), 22 deletions(-) rename src/{main.rs => bin/pentestkit.rs} (100%) delete mode 100644 todo.todo delete mode 100644 wordlist.txt rename {input => wordlist}/wordlist.txt (92%) diff --git a/src/main.rs b/src/bin/pentestkit.rs similarity index 100% rename from src/main.rs rename to src/bin/pentestkit.rs diff --git a/todo.todo b/todo.todo deleted file mode 100644 index e6b5f89..0000000 --- a/todo.todo +++ /dev/null @@ -1,13 +0,0 @@ -Demonstrate all four tools working correctly in your development environment - -Explain your implementation decisions and code architecture - -Describe your development and testing environment setup - -Participate in a role-play session as a Cyber Security Expert - -Discuss ethical and legal considerations of pentesting - -Show understanding of the underlying concepts and techniques - -Explain why certain tools may require elevated privileges \ No newline at end of file diff --git a/wordlist.txt b/wordlist.txt deleted file mode 100644 index c3f5d6f..0000000 --- a/wordlist.txt +++ /dev/null @@ -1,8 +0,0 @@ -/ -hot -admin -login -si -b -test -index.html \ No newline at end of file diff --git a/input/wordlist.txt b/wordlist/wordlist.txt similarity index 92% rename from input/wordlist.txt rename to wordlist/wordlist.txt index 0740594..9a7f9f3 100644 --- a/input/wordlist.txt +++ b/wordlist/wordlist.txt @@ -72,4 +72,12 @@ media _vti_bin webalizer common -logs \ No newline at end of file +logs +/ +hot +admin +login +si +b +test +index.html \ No newline at end of file From 77e9b0e2dc1a9757a467829923518e60f2d16b58 Mon Sep 17 00:00:00 2001 From: kill-ux Date: Sun, 1 Feb 2026 16:20:00 +0100 Subject: [PATCH 2/4] put main in bin --- src/bin/pentestkit.rs | 61 +------------------------------------------ src/cli.rs | 58 +++++++++++++++++++++++++++++++++++++++- src/utils/preluad.rs | 12 +++++++++ 3 files changed, 70 insertions(+), 61 deletions(-) diff --git a/src/bin/pentestkit.rs b/src/bin/pentestkit.rs index 9974f81..6d5af47 100644 --- a/src/bin/pentestkit.rs +++ b/src/bin/pentestkit.rs @@ -1,12 +1,4 @@ -use std::process::exit; - -use anyhow::{Ok, Result}; -use clap::Parser; -use log::warn; -use pentest_kit::{ - Cli, Mode, dirfinder, headergrabber, hostmapper, tinyscanner, - utils::{logger, start_gui}, -}; +use pentest_kit::utils::preluad::*; /// The entry point for the PentestKit application. /// @@ -27,54 +19,3 @@ async fn main() -> Result<()> { } Ok(()) } - -/// The command dispatcher. -/// Maps the CLI arguments to the specific tool logic (TinyScanner, DirFinder, etc.). -pub async fn start_commands(cli: &Cli) -> Result<()> { - let mode = Mode::try_from(cli)?; - match mode { - Mode::TinyScanner { - ref target, - ref ports, - verify, - } => { - println!("Running TinyScanner on {target} ports {ports}"); - tinyscanner::run( - target, - ports, - cli.threads, - &cli.output, - &cli.scan_mode, - verify, - None, - ) - .await?; - exit(0) - } - Mode::DirFinder { url, wordlist } => { - println!("DirFinder → {url} with wordlist {wordlist}"); - dirfinder::run( - &url, - &wordlist, - cli.threads, - &cli.output, - &cli.extensions, - None, - ) - .await?; - } - Mode::HostMapper { subnet } => { - println!("HostMapper on subnet {subnet}"); - hostmapper::run(subnet, cli.threads, &cli.output, None).await?; - } - Mode::HeaderGrabber { url } => { - println!("HeaderGrabber on {url}"); - headergrabber::run(&url, &cli.output, None).await?; - } - } - - if let Some(out) = &cli.output { - println!("→ Results will be saved to: {out}"); - } - Ok(()) -} diff --git a/src/cli.rs b/src/cli.rs index 28fd08c..64cb282 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,8 +1,9 @@ use anyhow::Ok; use clap::Parser; use ipnet::Ipv4Net; +use crate::utils::preluad::*; -use crate::hostmapper::localnet::get_localnet; +use crate::{hostmapper::localnet::get_localnet, tinyscanner}; #[derive(Parser, Default)] #[command( @@ -175,3 +176,58 @@ impl TryFrom<&Cli> for Mode { } } } + + + + + +/// The command dispatcher. +/// Maps the CLI arguments to the specific tool logic (TinyScanner, DirFinder, etc.). +pub async fn start_commands(cli: &Cli) -> Result<()> { + let mode = Mode::try_from(cli)?; + match mode { + Mode::TinyScanner { + ref target, + ref ports, + verify, + } => { + println!("Running TinyScanner on {target} ports {ports}"); + tinyscanner::run( + target, + ports, + cli.threads, + &cli.output, + &cli.scan_mode, + verify, + None, + ) + .await?; + exit(0) + } + Mode::DirFinder { url, wordlist } => { + println!("DirFinder → {url} with wordlist {wordlist}"); + dirfinder::run( + &url, + &wordlist, + cli.threads, + &cli.output, + &cli.extensions, + None, + ) + .await?; + } + Mode::HostMapper { subnet } => { + println!("HostMapper on subnet {subnet}"); + hostmapper::run(subnet, cli.threads, &cli.output, None).await?; + } + Mode::HeaderGrabber { url } => { + println!("HeaderGrabber on {url}"); + headergrabber::run(&url, &cli.output, None).await?; + } + } + + if let Some(out) = &cli.output { + println!("→ Results will be saved to: {out}"); + } + Ok(()) +} diff --git a/src/utils/preluad.rs b/src/utils/preluad.rs index dd25eef..9dc499c 100644 --- a/src/utils/preluad.rs +++ b/src/utils/preluad.rs @@ -1,6 +1,9 @@ pub use crate::tinyscanner::comman::*; pub use crate::tinyscanner::dispatcher::*; pub use crate::tinyscanner::scan_syn::send_syn_packet; +pub use crate::dirfinder; +pub use crate::headergrabber; +pub use crate::hostmapper; pub use crate::utils::*; pub use anyhow::{Context, Ok, Result, bail}; pub use colored::Colorize; @@ -12,6 +15,7 @@ pub use pnet::{ packet::ip::IpNextHeaderProtocols, transport::{self, TransportChannelType::Layer4, TransportProtocol::Ipv4, transport_channel}, }; +pub use std::process::exit; pub use rand::Rng; pub use std::{ io::ErrorKind, @@ -30,6 +34,14 @@ pub use tokio::{ time::timeout, }; +pub use clap::Parser; +pub use log::warn; +pub use crate::{ + Cli, start_commands, + utils::{logger, start_gui}, +}; + + pub use log::error; pub use tokio::{ fs::File, From 753f67875fc2f0ccc95f8fef7a14d16d6cee4296 Mon Sep 17 00:00:00 2001 From: kill-ux Date: Sun, 1 Feb 2026 16:53:36 +0100 Subject: [PATCH 3/4] j --- Cargo.lock | 67 +++++++----------------------------------------------- Cargo.toml | 65 +++++++++++++++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a042cf..3d6fc5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -698,19 +698,6 @@ dependencies = [ "libc", ] -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link 0.2.1", -] - [[package]] name = "clap" version = "4.5.54" @@ -1898,30 +1885,6 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.1.1" @@ -2431,9 +2394,9 @@ checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-traits" @@ -2907,7 +2870,6 @@ name = "pentest-kit" version = "0.1.0" dependencies = [ "anyhow", - "chrono", "clap", "colored", "csv", @@ -4098,9 +4060,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -4115,15 +4077,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -4979,19 +4941,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-future" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index d544193..7e296f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,33 +4,56 @@ version = "0.1.0" edition = "2024" [dependencies] -anyhow = "1.0.100" -chrono = "0.4.43" -clap = { version = "4.5.54", features = ["derive"] } -colored = "3.1.1" -csv = "1.4.0" -dashmap = "6.1.0" -eframe = "0.33.3" -egui = "0.33.3" -indicatif = "0.18.3" +# --- [ Error Handling & Logging ] --- +anyhow = "1.0.100" # Flexible error handling for applications +log = "0.4.29" # Standard logging facade +simple_logger = "5.1.0" # Simple logger for terminal output + +# --- [ Networking Core ] --- +# Low-level packet construction and raw socket access +pnet = { version = "0.35.0", features = ["std"] } +pnet_packet = "0.35.0" # Specialized packet parsing/manipulation +surge-ping = "0.8.4" # High-level ICMP (Ping) implementation for host discovery +# Subnet and IP range calculations (CIDR math) ipnet = "2.11.0" ipnetwork = "0.21.1" -log = "0.4.29" -network-interface = "2.0.5" -pnet = { version = "0.35.0", features = ["std"] } -pnet_packet = "0.35.0" +network-interface = "2.0.5" # Helper to find local adapters/interfaces for source IP selection + +# --- [ Asynchronous Runtime ] --- +# The heart of the concurrent scanner +tokio = { version = "1.49.0", features = ["full", "io-util"] } +tokio-util = { version = "0.7.18", features = ["full"] } + + +# --- [ Data Structures & Parallelism ] --- +# Concurrent Hashmap for sharing 'pending scans' across threads +dashmap = "6.1.0" +# Thread-safe random numbers (e.g., for TCP Sequence Numbers) rand = "0.9.2" -regex = "1.12.2" + +# --- [ GUI Framework (egui) ] --- +# Immediate mode GUI for the dashboard +egui = "0.33.3" +eframe = "0.33.3" # The framework to run egui on OS windows +rfd = "0.17.2" # Native file dialogs (Save/Open results) + +# --- [ CLI & UI Helpers ] --- +clap = { version = "4.5.54", features = ["derive"] } # CLI Argument parsing +indicatif = "0.18.3" # Progress bars for long network scans +colored = "3.1.1" # Terminal colors (RED/GREEN status) + + +# --- [ Web & Data Handling ] --- +# For Banner Grabbing or checking web server headers reqwest = { version = "0.13.1", features = ["native-tls"] } -rfd = "0.17.2" +# Serialization/Deserialization for config and output serde = { version = "1.0.228", features = ["derive"] } -serde-xml-rs = "0.8.2" serde_json = "1.0.149" -simple_logger = "5.1.0" -surge-ping = "0.8.4" -tokio = { version = "1.49.0", features = ["full", "io-util"] } -tokio-util = { version = "0.7.18", features = ["full"] } +serde-xml-rs = "0.8.2" # Parsing Nmap-style XML output +csv = "1.4.0" # Exporting scan results to Excel-friendly format +regex = "1.12.2" # Parsing text signatures/banners + [dev-dependencies] +# Mocking HTTP responses for testing the web-scanner parts wiremock = "0.6.5" - From 3e160484aed0233ec0e5f5279b4380ad15aabfdc Mon Sep 17 00:00:00 2001 From: kill-ux Date: Sun, 1 Feb 2026 18:06:32 +0100 Subject: [PATCH 4/4] add some tests --- README.md | 18 ++++++++ result1.json | 14 ++++++ result2.txt | 1 + result3.txt | 5 +++ result4.txt | 17 ++++++++ src/tinyscanner/dispatcher.rs | 2 +- src/tinyscanner/mod.rs | 8 ++-- src/utils/mod.rs | 12 ++++- tests/comman_integration.rs | 32 ++++++++++++++ tests/fingerprint_integration.rs | 70 ++++++++++++++++++++++++++++++ tests/headergrabber_integration.rs | 25 +++++++++++ tests/power_test.rs | 61 ++++++++++++++++++++++++++ tests/tinyscanner_integration.rs | 22 ++++++++++ tests/utils_integration.rs | 48 ++++++++++++++++++++ wordlist/wordlist.txt | 1 - 15 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 result1.json create mode 100644 result2.txt create mode 100644 result3.txt create mode 100644 result4.txt create mode 100644 tests/comman_integration.rs create mode 100644 tests/fingerprint_integration.rs create mode 100644 tests/headergrabber_integration.rs create mode 100644 tests/power_test.rs create mode 100644 tests/tinyscanner_integration.rs create mode 100644 tests/utils_integration.rs diff --git a/README.md b/README.md index cea8368..55628e4 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,11 @@ pentest-kit --headers https://example.com -o audit.json * **Network Isolation** : Ensure your testing environment is firewalled from the public internet. * **DNS** : If testing local VMs, ensure your `/etc/hosts` file is configured correctly for target resolution. +**Verified Test Targets:** +* **Localhost** : Tested against a multi-threaded Python 3 HTTP server on port 8080. +* **Container** : Audited a local OWASP Juice Shop Docker container to verify HeaderGrapper and DirFinder accuracy. +* **VM** : Conducted ping sweeps across a Host-Only subnet (192.168.56.0/24) to verify HostMapper. + ## Legal & Ethical Warning [!IMPORTANT] FOR AUTHORIZED EDUCATIONAL USE ONLY. @@ -136,6 +141,9 @@ The use of this software for scanning targets without prior mutual consent is il * **Target Unreachable:** Verify the target VM's IP address and ensure your network settings (NAT/Bridged) allow communication. * **Zero Results in DirFinder:** Ensure your wordlist path is correct and that the target doesn't have a WAF (Web Application Firewall) blocking rapid requests. * **OpenSSL Errors:** Ensure `libssl-dev` (Linux) or `openssl` (macOS) is installed on your development machine. +* **Non-Root Users** : If the -sS (SYN scan) flag is used without sudo or the cap_net_raw capability, the tool will exit with an error as it cannot open raw sockets. +* **Network Interface Access** : On Linux, the tool may fail to list interfaces if the user does not have read access to /sys/class/net/. + ### Known Limitations @@ -147,6 +155,8 @@ The use of this software for scanning targets without prior mutual consent is il ## Output Format Explanations +* **Storage Location** : By default, all output files are saved in the current working directory from which the command was executed. You may provide an absolute path (e.g., -o /home/user/reports/scan.json) to save elsewhere. + The toolkit uses a smart-detection system based on file extensions: * **JSON (`.json`):** Full data dump including nested maps of all retrieved HTTP headers. Best for post-processing with other tools. @@ -163,3 +173,11 @@ Pentest-Kit is a high-performance reconnaissance tool designed to provide rapid 1. **Speed:** Utilizing Rust's `Tokio` runtime for non-blocking I/O. 2. **Security Auditing:** Identifying misconfigured headers and sensitive file exposures (like `.env` or `.git`). 3. **Versatility:** Offering multiple scanning modes (SYN, Connect, Dir-Brute) in a single binary. + + +## HELP: Running without Sudo +To run SYN scans without being root every time, grant the binary specific capabilities: + +```bash +sudo setcap cap_net_raw,cap_net_admin=eip ./target/debug/pentest-kit +``` diff --git a/result1.json b/result1.json new file mode 100644 index 0000000..1b2febb --- /dev/null +++ b/result1.json @@ -0,0 +1,14 @@ +[ + { + "port": 22, + "state": "OPEN" + }, + { + "port": 80, + "state": "CLOSED" + }, + { + "port": 443, + "state": "CLOSED" + } +] \ No newline at end of file diff --git a/result2.txt b/result2.txt new file mode 100644 index 0000000..5ff3eb6 --- /dev/null +++ b/result2.txt @@ -0,0 +1 @@ +[200] http://example.com// \ No newline at end of file diff --git a/result3.txt b/result3.txt new file mode 100644 index 0000000..0f4a710 --- /dev/null +++ b/result3.txt @@ -0,0 +1,5 @@ +10.1.8.6 - LIVE (144.962µs) +10.1.8.3 - LIVE (208.354µs) +10.1.8.12 - LIVE (151.156µs) +10.1.8.5 - LIVE (165.044µs) +10.1.8.7 - LIVE (463.297µs) \ No newline at end of file diff --git a/result4.txt b/result4.txt new file mode 100644 index 0000000..31ac064 --- /dev/null +++ b/result4.txt @@ -0,0 +1,17 @@ +--- HEADER ANALYSIS REPORT --- +URL : http://example.com +Status : 200 OK +Server : cloudflare +Headers: +cf-cache-status: HIT, +server: cloudflare, +age: 6331, +last-modified: Fri, 30 Jan 2026 23:42:19 GMT, +date: Sun, 01 Feb 2026 16:49:24 GMT, +cf-ray: 9c72f1212e6e4893-LIS, +allow: GET, HEAD, +accept-ranges: bytes, +content-type: text/html, +connection: keep-alive, +Score : 0/100 +Missing: Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy diff --git a/src/tinyscanner/dispatcher.rs b/src/tinyscanner/dispatcher.rs index d75d439..92f9a92 100644 --- a/src/tinyscanner/dispatcher.rs +++ b/src/tinyscanner/dispatcher.rs @@ -1,7 +1,7 @@ use std::{net::IpAddr, sync::Arc}; use crate::utils::preluad::*; - +#[allow(clippy::too_many_arguments)] /// Dispatches scan tasks for each port using a semaphore to control concurrency. /// /// # Arguments diff --git a/src/tinyscanner/mod.rs b/src/tinyscanner/mod.rs index 58babec..d07f5aa 100644 --- a/src/tinyscanner/mod.rs +++ b/src/tinyscanner/mod.rs @@ -117,10 +117,10 @@ fn setup_syn_subsystem( let mut iter = transport::tcp_packet_iter(&mut rx); while shutdown_rx.try_recv().is_err() { - if let Result::Ok((tcp_packet, addr)) = iter.next() { - if target_ip == addr { - process_sniffer_packet(tcp_packet, &sniffer_state); - } + if let Result::Ok((tcp_packet, addr)) = iter.next() + && target_ip == addr + { + process_sniffer_packet(tcp_packet, &sniffer_state); } } Ok(()) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bed9bae..1ed9ce2 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -191,6 +191,15 @@ pub async fn save_results_header(path: &str, res: HeaderResult) -> Result<()> { report.push_str(&format!("URL : {}\n", res.url)); report.push_str(&format!("Status : {}\n", res.status_text)); report.push_str(&format!("Server : {}\n", res.server)); + report.push_str("Headers: \n"); + report.push_str(&format!( + "{}\n", + res.headers + .iter() + .map(|(k, v)| format!("{}: {},", k, v)) + .collect::>() + .join("\n") + )); report.push_str(&format!("Score : {}/100\n", res.score)); report.push_str(&format!("Missing: {}\n", res.missing_headers.join(", "))); @@ -215,8 +224,7 @@ pub fn logger(cli: &Cli) { /// Launches the Eframe/Egui native window. /// This fulfills the 'GUI' requirement of the project using hardware acceleration. pub fn start_gui() -> Result<()> { - let mut options = eframe::NativeOptions::default(); - options.centered = true; + let options = eframe::NativeOptions::default(); eframe::run_native( "PentestKit", options, diff --git a/tests/comman_integration.rs b/tests/comman_integration.rs new file mode 100644 index 0000000..5317d6d --- /dev/null +++ b/tests/comman_integration.rs @@ -0,0 +1,32 @@ +use pentest_kit::tinyscanner::comman::{parse_ports, resolve_ip, get_local_ip}; + +#[test] +fn test_parse_ports_simple() { + let p = parse_ports("80,443").unwrap(); + assert_eq!(p, vec![80, 443]); +} + +#[test] +fn test_parse_ports_range() { + let p = parse_ports("8000-8002").unwrap(); + assert_eq!(p, vec![8000, 8001, 8002]); +} + +#[test] +fn test_parse_ports_all_dash() { + let p = parse_ports("-").unwrap(); + assert_eq!(p.len(), u16::MAX as usize); +} + +#[test] +fn test_resolve_ip_direct_ip() { + let ip = resolve_ip("127.0.0.1").unwrap(); + assert_eq!(ip.to_string(), "127.0.0.1"); +} + +#[test] +fn test_get_local_ip_some() { + let target: std::net::IpAddr = "127.0.0.1".parse().unwrap(); + let got = get_local_ip(target); + assert!(got.is_some()); +} diff --git a/tests/fingerprint_integration.rs b/tests/fingerprint_integration.rs new file mode 100644 index 0000000..0dca745 --- /dev/null +++ b/tests/fingerprint_integration.rs @@ -0,0 +1,70 @@ +use std::io::Write; +use std::time::Duration; + +use pentest_kit::tinyscanner; + +fn uniq_file(prefix: &str, ext: &str) -> String { + let t = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + std::env::temp_dir() + .join(format!("{}_{}.{}", prefix, t, ext)) + .to_string_lossy() + .to_string() +} + +async fn spawn_server(mut handler: F) -> u16 +where + F: FnMut(std::net::TcpStream) + Send + 'static, +{ + // bind to port 0 to get an ephemeral port + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); + listener.set_nonblocking(true).unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::task::spawn_blocking(move || { + if let Ok((stream, _)) = listener.accept() { + handler(stream); + } + }); + addr.port() +} + +#[tokio::test] +async fn test_tinyscanner_fingerprinting() { + // HTTP server: sends HTTP banner immediately + let http_port = spawn_server(|mut s| { + let _ = s.write_all(b"HTTP/1.1 200 OK\r\nContent-Length:0\r\n\r\n"); + std::thread::sleep(Duration::from_millis(200)); + }) + .await; + + // SSH server: sends SSH banner + let ssh_port = spawn_server(|mut s| { + let _ = s.write_all(b"SSH-2.0-OpenSSH_8.0\r\n"); + std::thread::sleep(Duration::from_millis(200)); + }) + .await; + + // MySQL-like server: send bytes where index 4 == 0x0a + let mysql_port = spawn_server(|mut s| { + let data = [0u8, 0, 0, 0, 0x0a, 1, 2, 3, 4]; + let _ = s.write_all(&data); + std::thread::sleep(Duration::from_millis(200)); + }) + .await; + + let ports_str = format!("{},{},{}", http_port, ssh_port, mysql_port); + + let out = uniq_file("tiny_fp_out", "json"); + + // Run scanner in connect mode with verify=true + let res = tinyscanner::run("127.0.0.1", &ports_str, 4, &Some(out.clone()), "T", true, None).await; + assert!(res.is_ok()); + + let content = tokio::fs::read_to_string(out).await.unwrap_or_default(); + // Ensure output contains the port numbers we scanned + assert!(content.contains(&http_port.to_string())); + assert!(content.contains(&ssh_port.to_string())); + assert!(content.contains(&mysql_port.to_string())); +} diff --git a/tests/headergrabber_integration.rs b/tests/headergrabber_integration.rs new file mode 100644 index 0000000..aed4170 --- /dev/null +++ b/tests/headergrabber_integration.rs @@ -0,0 +1,25 @@ +use pentest_kit::headergrabber; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +#[tokio::test] +async fn test_headergrabber_run_and_save_json() { + // Start a mock HTTP server + let mock = MockServer::start().await; + + // prepare response with headers + let response = ResponseTemplate::new(200) + .insert_header("server", "mock-server/1.0") + .insert_header("Content-Security-Policy", "default-src 'self'") + .insert_header("X-Frame-Options", "DENY"); + + Mock::given(method("HEAD")).and(path("/")).respond_with(response).mount(&mock).await; + + let out = std::env::temp_dir().join("hdr_test_out.json").to_string_lossy().to_string(); + let res = headergrabber::run(&mock.uri(), &Some(out.clone()), None).await; + assert!(res.is_ok()); + + let content = tokio::fs::read_to_string(out).await.unwrap(); + // Should contain server banner or score + assert!(content.contains("mock-server") || content.contains("Score") || content.contains("server")); +} diff --git a/tests/power_test.rs b/tests/power_test.rs new file mode 100644 index 0000000..87f4e4f --- /dev/null +++ b/tests/power_test.rs @@ -0,0 +1,61 @@ +use pentest_kit::{headergrabber, dirfinder}; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; +use std::io::Write; + +#[tokio::test] +async fn power_test_end_to_end() { + // start mock server + let mock = MockServer::start().await; + + // HeaderGrabber: respond to HEAD / with headers + let hdr_resp = ResponseTemplate::new(200) + .insert_header("server", "mock/1.0") + .insert_header("Content-Security-Policy", "default-src 'self'") + .insert_header("X-Frame-Options", "DENY"); + + Mock::given(method("HEAD")).and(path("/")).respond_with(hdr_resp).mount(&mock).await; + + // DirFinder: respond to GET /admin (200) and GET /secret (404) + let admin_resp = ResponseTemplate::new(200); + let not_found = ResponseTemplate::new(404); + Mock::given(method("GET")).and(path("/admin")).respond_with(admin_resp).mount(&mock).await; + Mock::given(method("GET")).and(path("/secret")).respond_with(not_found).mount(&mock).await; + + // create a temporary wordlist file + let mut wl_path = std::env::temp_dir(); + wl_path.push("pentest_power_wordlist.txt"); + let mut f = std::fs::File::create(&wl_path).expect("create wordlist"); + writeln!(f, "admin").unwrap(); + writeln!(f, "secret").unwrap(); + + // output files + let mut hdr_out = std::env::temp_dir(); + hdr_out.push("power_header_out.json"); + let mut dir_out = std::env::temp_dir(); + dir_out.push("power_dir_out.json"); + + // Run headergrabber + headergrabber::run(&mock.uri(), &Some(hdr_out.to_string_lossy().to_string()), None) + .await + .expect("headergrabber run"); + + // Run dirfinder + dirfinder::run( + &mock.uri(), + &wl_path.to_string_lossy(), + 4, + &Some(dir_out.to_string_lossy().to_string()), + &None, + None, + ) + .await + .expect("dirfinder run"); + + // Verify outputs exist and contain expected tokens + let hdr_content = tokio::fs::read_to_string(hdr_out).await.expect("read hdr out"); + assert!(hdr_content.contains("mock/1.0") || hdr_content.contains("server")); + + let dir_content = tokio::fs::read_to_string(dir_out).await.expect("read dir out"); + assert!(dir_content.contains("admin") || dir_content.contains("/admin")); +} diff --git a/tests/tinyscanner_integration.rs b/tests/tinyscanner_integration.rs new file mode 100644 index 0000000..950c6a5 --- /dev/null +++ b/tests/tinyscanner_integration.rs @@ -0,0 +1,22 @@ +use pentest_kit::tinyscanner; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn uniq_file(prefix: &str, ext: &str) -> String { + let t = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + std::env::temp_dir() + .join(format!("{}_{}.{}", prefix, t, ext)) + .to_string_lossy() + .to_string() +} + +#[tokio::test] +async fn test_tinyscanner_run_connect_mode() { + // Scan localhost on a single port (likely closed but safe) + let out = uniq_file("tiny_run_out", "json"); + let res = tinyscanner::run("127.0.0.1", "80", 2, &Some(out.clone()), "T", false, None).await; + // run should complete without panicking; it may return Ok even if ports filtered + assert!(res.is_ok()); + let content = tokio::fs::read_to_string(out).await.unwrap_or_default(); + // Content may be JSON or plain text; ensure file was written + assert!(!content.is_empty()); +} diff --git a/tests/utils_integration.rs b/tests/utils_integration.rs new file mode 100644 index 0000000..9e1532d --- /dev/null +++ b/tests/utils_integration.rs @@ -0,0 +1,48 @@ +use pentest_kit::utils::{TinyResult, HostResult, save_results_tiny, save_results_host, save_results_header, HeaderResult}; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn uniq(name: &str) -> String { + let t = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + std::env::temp_dir().join(format!("{}_{}.tmp", name, t)).to_string_lossy().to_string() +} + +#[tokio::test] +async fn test_save_results_tiny_json_and_text() { + let path = uniq("tiny_json") + ".json"; + let results = vec![TinyResult { port: 80, state: "OPEN".to_string() }]; + save_results_tiny(&path, results.clone()).await.unwrap(); + let content = tokio::fs::read_to_string(&path).await.unwrap(); + assert!(content.contains("\"port\"")); + + let path2 = uniq("tiny_txt"); + save_results_tiny(&path2, results).await.unwrap(); + let content2 = tokio::fs::read_to_string(&path2).await.unwrap(); + assert!(content2.contains("Port") || content2.contains("80")); +} + +#[tokio::test] +async fn test_save_results_host_json_and_text() { + let path = uniq("host_json") + ".json"; + let results = vec![HostResult { ip: "127.0.0.1".to_string(), status: " LIVE ".to_string(), latency: "10ms".to_string() }]; + save_results_host(&path, results.clone()).await.unwrap(); + let content = tokio::fs::read_to_string(&path).await.unwrap(); + assert!(content.contains("127.0.0.1") || content.contains("ip")); + + let path2 = uniq("host_txt"); + save_results_host(&path2, results).await.unwrap(); + let content2 = tokio::fs::read_to_string(&path2).await.unwrap(); + assert!(content2.contains("127.0.0.1")); +} + +#[tokio::test] +async fn test_save_results_header_json() { + let path = uniq("header_json") + ".json"; + let mut hdr = HeaderResult::default(); + hdr.url = "http://example.test".to_string(); + hdr.status_text = "200 OK".to_string(); + hdr.server = "test-server".to_string(); + hdr.score = 80; + save_results_header(&path, hdr).await.unwrap(); + let content = tokio::fs::read_to_string(&path).await.unwrap(); + assert!(content.contains("example.test") || content.contains("server")); +} diff --git a/wordlist/wordlist.txt b/wordlist/wordlist.txt index 9a7f9f3..003895a 100644 --- a/wordlist/wordlist.txt +++ b/wordlist/wordlist.txt @@ -73,7 +73,6 @@ _vti_bin webalizer common logs -/ hot admin login