From 00a87615ecf7e926dd1580c9da13b3256919905c Mon Sep 17 00:00:00 2001 From: killux Date: Wed, 28 Jan 2026 16:26:36 +0100 Subject: [PATCH 1/4] l --- Cargo.lock | 106 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++ README.md | 37 ++++++++++++++ src/lib.rs | 4 +- tests/test_dirfinder.rs | 48 ++++++++++++++++++ 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 tests/test_dirfinder.rs diff --git a/Cargo.lock b/Cargo.lock index 8aa4527..9a042cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,16 @@ dependencies = [ "libloading", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -940,6 +950,24 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "deranged" version = "0.5.5" @@ -1380,6 +1408,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1387,6 +1430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1395,6 +1439,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -1443,9 +1498,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1752,6 +1811,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -1766,6 +1831,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -2097,6 +2163,12 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.180" @@ -2373,6 +2445,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.5" @@ -2850,6 +2932,7 @@ dependencies = [ "surge-ping", "tokio", "tokio-util", + "wiremock", ] [[package]] @@ -5344,6 +5427,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index d3cc330..d544193 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,7 @@ 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"] } + +[dev-dependencies] +wiremock = "0.6.5" + diff --git a/README.md b/README.md index d8ec820..baa6999 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,45 @@ echo "export PATH=$PATH:~/projects/pentest-kit/target/release" >> ~/.zshrc ## Usage The toolkit automatically detects the desired output format based on the file extension provided (.json, .xml, .csv, or .txt). +```bash +Welcome to PentestKit, High-performance pentest toolkit in Rust + +Usage: pentest-kit [OPTIONS] + +Options: + -t, --target Run the simple port scanner. + Use this option to specify the target IP address and the ports you wish to scan. + Example: -t 192.168.1.1 -p 22,80,443 + -p, --ports Ports to scan (comma-separated, ranges like 1-1024, or mix: 22,80,443,1000-2000) [default: 1-1024] + -d, --dir-url Run the directory brute-forcer. + Use this option to specify the target URL and the path to a wordlist for discovering hidden directories. + Example: -d http://example.com -w /path/to/wordlist.txt + -w, --wordlist Path to wordlist file + -n, --subnet Run the network host mapper. + Use this option to perform a ping sweep on a specified subnet to identify live hosts. + Example: -n 192.168.1.0/24 + -g, --header-url Run the HTTP header analyzer. + Use this option to retrieve and analyze HTTP headers from a specified URL, useful for identifying security headers. + Example: -g http://example.com + -o, --output File name to save output. + Use this option to specify the file name where the results of the scan or analysis will be saved. + Example: -o result.txt + -s Scan type: -sS for SYN scan, -sT for TCP connect scan + Use this option to specify the scanning method for port scanning. + Example: -sS for stealth SYN scan, -sT for TCP connect scan [default: T] + -l, --log + -c, --threads [default: 50] + -x, --extensions + -u, --gui + -v, --verify + --localnet + -h, --help Print help + -V, --version Print version + ``` + ### Directory Brute-forcing (DirFinder) + Scan for hidden files and sensitive directories using high concurrency. ```bash diff --git a/src/lib.rs b/src/lib.rs index e9f6a32..2939cb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -mod cli; -mod utils; +pub mod cli; +pub mod utils; pub mod dirfinder; pub mod hostmapper; pub mod tinyscanner; diff --git a/tests/test_dirfinder.rs b/tests/test_dirfinder.rs new file mode 100644 index 0000000..e1e91d0 --- /dev/null +++ b/tests/test_dirfinder.rs @@ -0,0 +1,48 @@ +#[cfg(test)] +mod tests { + use pentest_kit::dirfinder; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{method, path}, + }; + + #[tokio::test] + async fn test_dirfinder_integration() { + // 1. Start a local mock server + let server = MockServer::start().await; + + // 2. Mock a "FOUND" folder + Mock::given(method("GET")) + .and(path("/admin")) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + // 3. Mock a "NOT FOUND" folder + Mock::given(method("GET")) + .and(path("/secret")) + .respond_with(ResponseTemplate::new(404)) + .mount(&server) + .await; + + let wordlist = "admin\nsecret"; + let wordlist_path = "test_wordlist.txt"; + std::fs::write(wordlist_path, wordlist).unwrap(); + + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + + let _ = dirfinder::run(&server.uri(), wordlist_path, 2, &None, &None, Some(tx)).await; + + let mut found_admin = false; + while let Ok(msg) = rx.try_recv() { + // Check if the UI message contains our success criteria + if msg.contains("FOUND") && msg.contains("/admin") { + found_admin = true; + } + } + + assert!(found_admin); + + std::fs::remove_file(wordlist_path).unwrap(); + } +} From b8f0d97f2e17af9f4127fc4a799cff9fcdc2776d Mon Sep 17 00:00:00 2001 From: killux Date: Wed, 28 Jan 2026 17:31:20 +0100 Subject: [PATCH 2/4] suntaax cleppy with cargo cleppy --- README.md | 70 ++++++++++++++++++++----- wordlist.txt => input/wordlist.txt | 0 output.txt => output/output.txt | 0 result.json | 14 ----- result.xml | 9 ---- result4.xml | 26 ---------- src/cli.rs | 7 --- src/dirfinder/mod.rs | 16 +++++- src/gui/mod.rs | 20 ++++--- src/headergrabber/mod.rs | 16 ++++++ src/hostmapper/localnet.rs | 14 ++--- src/hostmapper/mod.rs | 19 +++++-- src/main.rs | 29 +++-------- src/tinyscanner/mod.rs | 83 ++++++++++++++++++++---------- src/tinyscanner/scan_syn.rs | 25 ++++----- src/utils/mod.rs | 33 +++++++++++- test_site/admin | 0 test_site/config | 0 test_site/file.js | 0 test_site/login | 0 test_site/test/index.html | 0 todo.todo | 13 +++++ 22 files changed, 241 insertions(+), 153 deletions(-) rename wordlist.txt => input/wordlist.txt (100%) rename output.txt => output/output.txt (100%) delete mode 100644 result.json delete mode 100644 result.xml delete mode 100644 result4.xml delete mode 100644 test_site/admin delete mode 100644 test_site/config delete mode 100644 test_site/file.js delete mode 100644 test_site/login delete mode 100644 test_site/test/index.html create mode 100644 todo.todo diff --git a/README.md b/README.md index baa6999..cea8368 100644 --- a/README.md +++ b/README.md @@ -3,37 +3,43 @@ A high-performance, asynchronous security toolkit built with **Rust** and the **Tokio** runtime. This kit is designed for rapid network reconnaissance, directory discovery, and web security auditing. ## Table of Contents -* [Prerequisites](#-prerequisites) -* [Development Setup](#-development-setup) -* [Usage](#-usage) -* [Features & Bonuses](#-features--bonuses) -* [Testing Environment](#-testing-environment) -* [Legal & Ethical Warning](#-legal--ethical-warning) + +* [Prerequisites](#prerequisites) +* [Development Setup](#development-setup) +* [Usage](#usage) +* [Testing Environment](#testing-environment) +* [Legal & Ethical Warning](#legal--ethical-warning) ## Prerequisites + * **Rust Toolchain:** latses version 1.9 * **OpenSSL:** Required for HTTPS requests (`libssl-dev` on Linux). * **Privilege Requirements:** * **DirFinder / HeaderGrapper / HostMapper :** Standard user permissions (uses standard TCP/HTTP stack). - * **TCP-SYN Scanner (falsg: `-sS`):** Requires **Root/Sudo** privileges to craft raw network packets. +* **TCP-SYN Scanner (falsg: `-sS`):** Requires **Root/Sudo** privileges to craft raw network packets. --- ## Development Setup + ### 1. VM Environment (Recommended) + To ensure safety and network isolation, develop and test within a Virtual Machine: + * **OS:** Kali Linux or Ubuntu 22.04 LTS. * **Network:** Set the VM network adapter to **Host-Only** or **NAT Network** to prevent scanning external networks accidentally. ### 2. Dependency Installation + ```bash # Ubuntu/Kali sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev ``` ### 3. Compilation + ```bash # Clone and build the project git clone https://github.com/kill-ux/pentest-kit.git @@ -44,6 +50,7 @@ echo "export PATH=$PATH:~/projects/pentest-kit/target/release" >> ~/.zshrc ``` ## Usage + The toolkit automatically detects the desired output format based on the file extension provided (.json, .xml, .csv, or .txt). ```bash @@ -92,25 +99,24 @@ pentest-kit -d https://google.com -w wordlist.txt -o result.json ``` ### Security Header Analysis (HeaderGrapper) + Analyze server banners and evaluate the security posture of HTTP headers. ```bash pentest-kit --headers https://example.com -o audit.json ``` -# Testing Environment +## Testing Environment ### Internal Lab Configuration: -* **Targets** : It is highly recommended to use local targets such as **OWASP** Juice Shop or **Metasploitable3** running on a local Docker bridge. +* **Targets** : It is highly recommended to use local targets such as **OWASP** Juice Shop or **Metasploitable3** running on a local Docker bridge. * **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. +## Legal & Ethical Warning -# Legal & Ethical Warning - -[!IMPORTANT]() FOR AUTHORIZED EDUCATIONAL USE ONLY. +[!IMPORTANT] FOR AUTHORIZED EDUCATIONAL USE ONLY. The use of this software for scanning targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state, and federal laws. @@ -119,3 +125,41 @@ The use of this software for scanning targets without prior mutual consent is il 1. Always obtain written permission before performing a scan. 2. Never use these tools against public infrastructure. 3. This software is provided "as is" without warranty of any kind. The developer assumes no liability for any damage caused by the misuse of this tool. + +--- + +## 🛠 Troubleshooting & Limitations + +### Troubleshooting Tips + +* **Permission Denied:** Ensure you use `sudo` when running SYN scans (`-sS`). Raw sockets require root privileges. +* **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. + +### Known Limitations + +* **Rate Limiting:** Without a manual delay feature, high thread counts may cause servers to temporarily ban your IP. +* **WAF Evasion:** The toolkit does not currently support proxy-chaining or automated User-Agent rotation. +* **SSL/TLS:** The tool is configured to skip certificate verification to allow testing on local/self-signed lab environments. + +--- + +## Output Format Explanations + +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. +* **XML (`.xml`):** Hierarchical structured report. Uses a root `` tag for compatibility with enterprise XML parsers. +* **CSV (`.csv`):** Flattened data rows. Ideal for importing scan results into Excel or Google Sheets for data analysis. +* **TXT (`.txt`):** Human-readable summary that strips ANSI colors for clean log viewing. + +--- + +## Project Overview and Objectives + +Pentest-Kit is a high-performance reconnaissance tool designed to provide rapid infrastructure insights. Its main objectives are: + +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. diff --git a/wordlist.txt b/input/wordlist.txt similarity index 100% rename from wordlist.txt rename to input/wordlist.txt diff --git a/output.txt b/output/output.txt similarity index 100% rename from output.txt rename to output/output.txt diff --git a/result.json b/result.json deleted file mode 100644 index dbd1eca..0000000 --- a/result.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "target": "https://users.rust-lang.org/faq", - "status": 200, - "is_sensitive": false, - "category": "INFO" - }, - { - "target": "https://users.rust-lang.org//", - "status": 200, - "is_sensitive": false, - "category": "INFO" - } -] \ No newline at end of file diff --git a/result.xml b/result.xml deleted file mode 100644 index b9ff011..0000000 --- a/result.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - https://users.rust-lang.org// - 200 - false - INFO - - \ No newline at end of file diff --git a/result4.xml b/result4.xml deleted file mode 100644 index 30574fe..0000000 --- a/result4.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - https://google.com - 301 Moved Permanently - gws - 20 - Content-Security-Policy - Strict-Transport-Security - X-Content-Type-Options - Referrer-Policy - - text/html; charset=UTF-8 - gws - 0 - object-src 'none';base-uri 'self';script-src - 'nonce-8DSMTll9jGvfFMcWf1KECQ' 'strict-dynamic' 'report-sample' 'unsafe-eval' - 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp - SAMEORIGIN - https://www.google.com/ - public, max-age=2592000 - Wed, 28 Jan 2026 14:16:32 GMT - Fri, 27 Feb 2026 14:16:32 GMT - 220 - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 - - \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 7421fa6..b050b4c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -172,10 +172,3 @@ impl TryFrom<&Cli> for Mode { } } } - -pub fn strip_ansi(input: &str) -> String { - // This regex matches the standard CSI (Control Sequence Introducer) - // sequences used for colors and formatting. - let re = regex::Regex::new(r"\x1b\[[0-9;]*[mK]").unwrap(); - re.replace_all(input, "").to_string() -} diff --git a/src/dirfinder/mod.rs b/src/dirfinder/mod.rs index d20ae7f..9a0d291 100644 --- a/src/dirfinder/mod.rs +++ b/src/dirfinder/mod.rs @@ -20,6 +20,17 @@ use crate::{ utils::{ScanResult, save_results}, }; + +/// Executes an asynchronous directory discovery scan. +/// +/// This function reads a wordlist line-by-line, optionally appends extensions, +/// and dispatches concurrent HTTP GET requests to the target URL. +/// +/// # Arguments +/// * `url` - The base target URL (e.g., "https://example.com"). +/// * `wordlist_path` - Path to the local text file containing directory names. +/// * `threads` - The maximum number of concurrent HTTP requests (controlled by Semaphore). +/// * `extensions_str` - Optional comma-separated list of file extensions to append. pub async fn run( url: &str, wordlist_path: &str, @@ -49,7 +60,7 @@ pub async fn run( .clone() .map(|s| { s.split(',') - .map(|ext| format!("{}", ext.trim_start_matches("."))) + .map(|ext| ext.trim_start_matches(".").to_string()) .collect() }) .unwrap_or_default(); @@ -70,7 +81,9 @@ pub async fn run( .unwrap() .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), ); + let (tx, mut rx) = mpsc::channel::(100); + let mut lines = reader.lines(); while let Some(line) = lines.next_line().await? { let path = line.trim(); @@ -175,7 +188,6 @@ pub async fn run( } if let Some(path) = output_path { - // tokio::fs::write(path, results.join("\n")).await?; save_results(path, results).await?; println_tx!(tx_ui, info!, "Results saved to: {}", path); } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index c5898e0..598be93 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -117,10 +117,10 @@ impl App for PentestApp { cli.wordlist = Some(path_str); } - if ui.button("Browse").clicked() { - if let Some(path) = rfd::FileDialog::new().pick_file() { - cli.wordlist = Some(path.display().to_string()); - } + if ui.button("Browse").clicked() + && let Some(path) = rfd::FileDialog::new().pick_file() + { + cli.wordlist = Some(path.display().to_string()); } }); } @@ -196,13 +196,8 @@ impl App for PentestApp { if let Some(s) = subnet && let Result::Ok(net) = s.parse() { - hostmapper::run( - net, - threads, - &output_path, - Some(tx_clone), - ) - .await + hostmapper::run(net, threads, &output_path, Some(tx_clone)) + .await } else { Err(anyhow::anyhow!("Subnet is required for HostMapper")) } @@ -270,6 +265,8 @@ impl App for PentestApp { } } +/// A specialized renderer that converts ANSI terminal color codes into Egui colors. +/// This ensures consistent visual feedback between the CLI and GUI versions. fn render_ansi_text(ui: &mut egui::Ui, text: &str) { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; // Remove space between characters for seamless color @@ -305,6 +302,7 @@ fn render_ansi_text(ui: &mut egui::Ui, text: &str) { }); } +/// get current color from code pub fn get_current_color(code: &str, current_color: Color32) -> Color32 { match code { // Standard Colors diff --git a/src/headergrabber/mod.rs b/src/headergrabber/mod.rs index eec46af..3e2d03f 100644 --- a/src/headergrabber/mod.rs +++ b/src/headergrabber/mod.rs @@ -8,6 +8,17 @@ use crate::{ utils::{HeaderResult, save_results_header}, }; + +/// Executes a security audit of a target's HTTP headers. +/// +/// This tool uses a non-intrusive HEAD request to retrieve metadata without +/// downloading the full page body, making it fast and low-profile. It evaluates +/// infrastructure banners and checks for the presence of critical security headers. +/// +/// # Arguments +/// * `url` - The target web address to audit. +/// * `output_path` - Optional file path for the JSON/XML/CSV report. +/// * `tx_ui` - Channel for real-time UI/CLI feedback. pub async fn run( url: &str, output_path: &Option, @@ -120,6 +131,11 @@ pub async fn run( Ok(()) } + +/// Performs a weighted security analysis on the provided HeaderMap. +/// +/// It checks against a hardcoded list of industry-standard security headers +/// (CSP, HSTS, etc.) and deducts points for each missing protection. fn perform_deep(headers: &HeaderMap) -> (Vec<(&'static str, String, &'static str)>, i32) { let mut warnings = Vec::new(); let mut score = 100; diff --git a/src/hostmapper/localnet.rs b/src/hostmapper/localnet.rs index 59d82b0..5dc6039 100644 --- a/src/hostmapper/localnet.rs +++ b/src/hostmapper/localnet.rs @@ -2,6 +2,8 @@ use anyhow::{Result, anyhow, bail}; use ipnet::Ipv4Net; use network_interface::{Addr, NetworkInterface, NetworkInterfaceConfig}; +/// Identifies the entire local subnet range to enable +/// automatic "ping sweeps" without manual CIDR input. pub fn get_localnet() -> Result { let interfaces = NetworkInterface::show().map_err(|e| anyhow!("Failed to list interfaces: {}", e))?; @@ -10,12 +12,12 @@ pub fn get_localnet() -> Result { continue; } for addr in itf.addr { - if let Addr::V4(v4_addr) = addr { - if let Some(netmask) = v4_addr.netmask { - dbg!(netmask); - return Ipv4Net::with_netmask(v4_addr.ip, netmask) - .map_err(|_| anyhow!("Invalid mask for {}", v4_addr.ip)); - } + if let Addr::V4(v4_addr) = addr + && let Some(netmask) = v4_addr.netmask + { + dbg!(netmask); + return Ipv4Net::with_netmask(v4_addr.ip, netmask) + .map_err(|_| anyhow!("Invalid mask for {}", v4_addr.ip)); } } } diff --git a/src/hostmapper/mod.rs b/src/hostmapper/mod.rs index 1cbd5c5..5792eb3 100644 --- a/src/hostmapper/mod.rs +++ b/src/hostmapper/mod.rs @@ -18,13 +18,23 @@ use crate::println_tx; pub mod localnet; +/// Executes an asynchronous ICMP "Ping Sweep" across a specified IPv4 subnet. +/// +/// This function identifies live hosts by sending ICMP Echo Requests and waiting +/// for Echo Replies. It uses a `JoinSet` to manage concurrency and a `Semaphore` +/// to prevent overwhelming the local network stack. +/// +/// # Arguments +/// * `net` - The CIDR subnet to scan (e.g., 192.168.1.0/24). +/// * `threads` - Maximum number of simultaneous ping requests. +/// * `output_path` - Optional file path to save the list of live hosts. +/// * `tx_ui` - MPSC channel for sending real-time updates to the GUI/CLI. pub async fn run( net: Ipv4Net, threads: usize, output_path: &Option, tx_ui: Option>, ) -> Result<()> { - let semaphore = Arc::new(Semaphore::new(threads)); let mut set = JoinSet::new(); let (tx, mut rx) = mpsc::channel::(100); @@ -50,8 +60,11 @@ pub async fn run( let tx_clone = tx.clone(); let client_clone = client.clone(); + // Generate a unique identifier for the ping based on the IP address. + // This helps the surge-ping client match replies to requests. let ip_u32 = u32::from(ip); let ident = (ip_u32 & 0xFFFF) as u16; + let tx_ui_clone = tx_ui.clone(); let pb_clone = pb.clone(); set.spawn(async move { @@ -71,7 +84,7 @@ pub async fn run( println_tx!(tx_ui_clone, "{}", styled); }); - let _ = tx_clone.send(plain).await?; + tx_clone.send(plain).await?; } pb_clone.inc(1); @@ -87,7 +100,7 @@ pub async fn run( } // Ensure JoinSet finishes - while let Some(_) = set.join_next().await {} + while set.join_next().await.is_some() {} if let Some(path) = output_path { tokio::fs::write(path, results.join("\n")).await?; println_tx!(tx_ui, info!,"Live hosts saved to: {}", path); diff --git a/src/main.rs b/src/main.rs index a2c8f9d..96e72f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,12 @@ use std::process::exit; use anyhow::{Ok, Result}; use clap::Parser; use log::warn; -use pentest_kit::{Cli, Mode, dirfinder, gui::PentestApp, headergrabber, hostmapper, tinyscanner}; -use simple_logger::SimpleLogger; +use pentest_kit::{dirfinder, headergrabber, hostmapper, tinyscanner, utils::{logger, start_gui}, Cli, Mode}; +/// The entry point for the PentestKit application. +/// +/// We use `#[tokio::main]` to initialize the multi-threaded async runtime, +/// which allows all downstream tools to perform non-blocking network I/O. #[tokio::main] async fn main() -> Result<()> { let mut cli = Cli::parse(); @@ -22,27 +25,9 @@ async fn main() -> Result<()> { Ok(()) } -pub fn logger(cli: &Cli) { - SimpleLogger::new() - .with_level(if cli.log { - log::LevelFilter::Debug - } else { - log::LevelFilter::Info - }) - .init() - .expect("Failed to initialize logger"); -} - -pub fn start_gui() -> Result<()> { - let options = eframe::NativeOptions::default(); - eframe::run_native( - "PentestKit", - options, - Box::new(|_cc| Result::Ok(Box::new(PentestApp::default()))), - ) - .map_err(|err| anyhow::anyhow!("Faild to create a gui {}", err)) -} +/// 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 { diff --git a/src/tinyscanner/mod.rs b/src/tinyscanner/mod.rs index 5565342..da3b3e9 100644 --- a/src/tinyscanner/mod.rs +++ b/src/tinyscanner/mod.rs @@ -28,12 +28,13 @@ use pnet::{ transport::{self, TransportChannelType::Layer4, TransportProtocol::Ipv4, transport_channel}, }; -use crate::tinyscanner::scan_syn::{send_syn_packet, use_syn}; +use crate::tinyscanner::scan_syn::send_syn_packet; const TIMEOUT: Duration = Duration::from_millis(1000); +/// Global collection of binary and text-based probes used to elicit +/// responses from unknown services when a connection is established. static SERVICE_PROBES: &[(&[u8], &str)] = &[ - // Try the most popular ones first to avoid firewall "tarpitting" ( &[ 0x16, 0x03, 0x01, 0x00, 0x61, 0x01, 0x00, 0x00, 0x5d, 0x03, 0x03, 0x4f, 0x12, 0x7a, @@ -85,6 +86,16 @@ macro_rules! println_tx { } +/// The main entry point for the TinyScanner tool. +/// +/// Coordinates between TCP Connect and SYN scanning modes, manages +/// the progress bar, and handles the concurrency lifecycle. +/// +/// # Arguments +/// * `target` - Hostname or IP to scan. +/// * `ports_str` - User-defined port range (e.g., "1-1024,8080"). +/// * `scan_mod` - Determines if SYN or Connect scan is used. +/// * `verify` - If true, performs active service fingerprinting. pub async fn run( target: &str, ports_str: &str, @@ -100,7 +111,7 @@ pub async fn run( target ); - let use_syn = use_syn(scan_mod); + let use_syn = matches!(scan_mod, "S"); let ip = resolve_ip(target)?; println_tx!(tx_ui, "Resolved target to IP: {}", ip); @@ -109,7 +120,9 @@ pub async fn run( let (syn_request_tx, mut syn_request_rx) = mpsc::channel::<(IpAddr, u16, u16)>(10000); let pending_scans: Arc>> = Arc::new(DashMap::new()); + // Setup for SYN Scanning if use_syn { + // ... (raw socket setup) let protocol = Layer4(Ipv4(IpNextHeaderProtocols::Tcp)); let (mut tx_raw, _) = transport_channel(4096, protocol) .context("Failed to open raw socket. Try running with sudo.")?; @@ -124,6 +137,8 @@ pub async fn run( println_tx!(tx_ui, "Using local IP {} for checksums", local_ip); + // 1. SYN PACKET SENDER + // Spawns a dedicated task to push raw packets into the network interface. tokio::spawn(async move { while let Some((dest_ip, dest_port, src_port)) = syn_request_rx.recv().await { let _ = send_syn_packet(&mut tx_raw, dest_ip, dest_port, src_port, local_ip).await; @@ -131,6 +146,8 @@ pub async fn run( }); // 2. THE GLOBAL SNIFFER + // Runs in 'spawn_blocking' to avoid stalling the async runtime. + // It captures all incoming TCP traffic and matches SYN-ACKs to pending scans. let sniffer_state = pending_scans.clone(); tokio::task::spawn_blocking(move || { let protocol = Layer4(Ipv4(IpNextHeaderProtocols::Tcp)); @@ -142,21 +159,21 @@ pub async fn run( break; } - if let Result::Ok((tcp_packet, addr)) = iter.next() { - if ip == addr { - let dest_port = tcp_packet.get_destination(); - if let Some((_, tx_result)) = sniffer_state.remove(&dest_port) { - let flags = tcp_packet.get_flags(); - let status = - if (flags & TcpFlags::SYN) != 0 && (flags & TcpFlags::ACK) != 0 { - "open" - } else if (flags & TcpFlags::RST) != 0 { - "closed" - } else { - "filtered" - }; - let _ = tx_result.send(status); - } + if let Result::Ok((tcp_packet, addr)) = iter.next() + && ip == addr + { + let dest_port = tcp_packet.get_destination(); + if let Some((_, tx_result)) = sniffer_state.remove(&dest_port) { + let flags = tcp_packet.get_flags(); + let status = if (flags & TcpFlags::SYN) != 0 && (flags & TcpFlags::ACK) != 0 + { + "open" + } else if (flags & TcpFlags::RST) != 0 { + "closed" + } else { + "filtered" + }; + let _ = tx_result.send(status); } } } @@ -221,16 +238,15 @@ pub async fn run( if verify { if let Result::Ok(Result::Ok(n)) = timeout(Duration::from_millis(500), stream.read(&mut buf)).await + && n > 0 { - if n > 0 { - detected_service = identify_service(&buf[..n], &[]); // Identify the banner - found = true - } + detected_service = identify_service(&buf[..n], &[]); // Identify the banner + found = true } if !found { for (payload, _name) in SERVICE_PROBES { - let _ = stream.write_all(&payload).await; + let _ = stream.write_all(payload).await; let mut buf = [0; 256]; match timeout( Duration::from_millis(400), @@ -240,7 +256,7 @@ pub async fn run( { Result::Ok(Result::Ok(n)) if n > 0 => { detected_service = - identify_service(&buf[..n], &payload); + identify_service(&buf[..n], payload); break; } _ => { @@ -337,6 +353,8 @@ pub async fn run( Ok(()) } +/// Converts a target string into a valid IpAddr. +/// Attempts to parse as a direct IP first, then falls back to a DNS lookup. pub fn resolve_ip(target: &str) -> Result { if let Result::Ok(ip) = target.parse::() { info!("Target is a direct IP address: {}", ip); @@ -360,6 +378,8 @@ pub fn resolve_ip(target: &str) -> Result { Ok(ip) } +/// Parses a complex string of ports and ranges into a sorted, unique vector of u16. +/// Supports formats like "80,443,1000-2000" and the "-" wildcard for all ports. pub fn parse_ports(ports_str: &str) -> Result> { let mut ports = Vec::with_capacity(1024); @@ -416,15 +436,24 @@ pub fn parse_ports(ports_str: &str) -> Result> { use std::net::UdpSocket; +/// Finds the specific local IP used for a target to ensure +/// TCP Checksum validity during raw packet injection. fn get_local_ip(target: IpAddr) -> Option { - // We create a UDP socket and "connect" to the target. - // This doesn't send any data, but it forces the OS to - // choose the correct local interface IP. let socket = UdpSocket::bind("0.0.0.0:0").ok()?; socket.connect(SocketAddr::new(target, 80)).ok()?; socket.local_addr().ok().map(|addr| addr.ip()) } +/// Fingerprinting engine that identifies services based on raw data responses. +/// +/// Uses a multi-stage approach: +/// 1. String-based keyword matching (HTTP, SSH). +/// 2. Binary header verification (TLS Handshakes, MySQL packets). +/// 3. DNS-specific bitwise flag checks. +/// +/// # Arguments +/// * `data` - The raw bytes received from the server. +/// * `pg_probe` - The last sent probe to check for echo/reflection services. fn identify_service(data: &[u8], pg_probe: &[u8]) -> String { // 1. Convert to string once for reuse let response = String::from_utf8_lossy(data); diff --git a/src/tinyscanner/scan_syn.rs b/src/tinyscanner/scan_syn.rs index ad51df0..1eda380 100644 --- a/src/tinyscanner/scan_syn.rs +++ b/src/tinyscanner/scan_syn.rs @@ -4,13 +4,11 @@ use pnet::transport::TransportSender; use anyhow::{Context, Ok, Result, bail}; use std::net::IpAddr; -pub fn use_syn(scan_mod: &str) -> bool { - match scan_mod { - "S" => true, - _ => false, - } -} - +/// Calculates the TCP checksum using the mandatory "Pseudo-Header". +/// +/// In the TCP/IP stack, the TCP checksum isn't calculated on the TCP header alone; +/// it must include the Source IP and Destination IP from the IP layer to ensure +/// the packet hasn't been misrouted. pub fn tcp_checksum(tcp_packet: &MutableTcpPacket, src_ip: IpAddr, dest_ip: IpAddr) -> Result { match (src_ip, dest_ip) { (IpAddr::V4(sec), IpAddr::V4(dest)) => { @@ -25,6 +23,12 @@ pub fn tcp_checksum(tcp_packet: &MutableTcpPacket, src_ip: IpAddr, dest_ip: IpAd } } + +/// Manually constructs and sends a raw TCP SYN packet. +/// +/// This bypasses the OS's standard 3-way handshake logic, allowing us to +/// perform "half-open" scanning which is faster and less likely to be +/// logged by simple application-level loggers. pub async fn send_syn_packet( tx: &mut TransportSender, dest_ip: IpAddr, @@ -41,17 +45,14 @@ pub async fn send_syn_packet( tcp_packet.set_destination(dest_port); tcp_packet.set_sequence(rand::random()); tcp_packet.set_acknowledgement(0); - tcp_packet.set_data_offset(5); // 5 * 4 = 20 bytes + tcp_packet.set_data_offset(5); // Offset is in 32-bit words: 5 * 4 = 20 bytes tcp_packet.set_flags(TcpFlags::SYN); - tcp_packet.set_window(64240); + tcp_packet.set_window(64240); // Standard window size tcp_packet.set_urgent_ptr(0); - // 2. Calculate Checksum (The most important part for Raw Sockets) - // The checksum requires a "Pseudo Header" containing source and dest IPs let checksum = tcp_checksum(&tcp_packet, src_ip, dest_ip)?; tcp_packet.set_checksum(checksum); - // 3. Fire and Forget tx.send_to(tcp_packet, dest_ip) .context("Failed to send raw SYN packet")?; diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5142045..0a44cfd 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,9 @@ use anyhow::Result; use egui::ahash::HashMap; use serde::Serialize; +use simple_logger::SimpleLogger; + +use crate::{gui::PentestApp, Cli}; #[derive(Serialize, Clone)] pub struct ScanResult { @@ -95,7 +98,7 @@ pub async fn save_results_header(path: &str, res: HeaderResult) -> Result<()> { } else { // Default text report (The "Normal" way) let mut report = String::new(); - report.push_str(&format!("--- HEADER ANALYSIS REPORT ---\n")); + report.push_str("--- HEADER ANALYSIS REPORT ---\n"); 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)); @@ -106,3 +109,31 @@ pub async fn save_results_header(path: &str, res: HeaderResult) -> Result<()> { } Ok(()) } + + + +/// Initializes the logging system. +/// Uses `SimpleLogger` to provide terminal feedback during long-running scans. +pub fn logger(cli: &Cli) { + SimpleLogger::new() + .with_level(if cli.log { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }) + .init() + .expect("Failed to initialize logger"); +} + + +/// Launches the Eframe/Egui native window. +/// This fulfills the 'GUI' requirement of the project using hardware acceleration. +pub fn start_gui() -> Result<()> { + let options = eframe::NativeOptions::default(); + eframe::run_native( + "PentestKit", + options, + Box::new(|_cc| Result::Ok(Box::new(PentestApp::default()))), + ) + .map_err(|err| anyhow::anyhow!("Faild to create a gui {}", err)) +} diff --git a/test_site/admin b/test_site/admin deleted file mode 100644 index e69de29..0000000 diff --git a/test_site/config b/test_site/config deleted file mode 100644 index e69de29..0000000 diff --git a/test_site/file.js b/test_site/file.js deleted file mode 100644 index e69de29..0000000 diff --git a/test_site/login b/test_site/login deleted file mode 100644 index e69de29..0000000 diff --git a/test_site/test/index.html b/test_site/test/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/todo.todo b/todo.todo new file mode 100644 index 0000000..e6b5f89 --- /dev/null +++ b/todo.todo @@ -0,0 +1,13 @@ +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 From 3da60e57046e1e0ceb3f8653b30afdf75d98aa45 Mon Sep 17 00:00:00 2001 From: killux Date: Wed, 28 Jan 2026 17:50:13 +0100 Subject: [PATCH 3/4] gthub actions --- .github/workflows/rust_ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/rust_ci.yml diff --git a/.github/workflows/rust_ci.yml b/.github/workflows/rust_ci.yml new file mode 100644 index 0000000..b77f016 --- /dev/null +++ b/.github/workflows/rust_ci.yml @@ -0,0 +1,33 @@ +name: Rust CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + test_and_lint: + name: Test & Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Rust Toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Run Linter (Clippy) + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Run Tests + run: cargo test --verbose \ No newline at end of file From 37b726bd95f17fa0477c383e8158ca1fc97f8375 Mon Sep 17 00:00:00 2001 From: killux Date: Wed, 28 Jan 2026 17:56:05 +0100 Subject: [PATCH 4/4] test --- tests/test_dirfinder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_dirfinder.rs b/tests/test_dirfinder.rs index e1e91d0..2c6fed6 100644 --- a/tests/test_dirfinder.rs +++ b/tests/test_dirfinder.rs @@ -46,3 +46,5 @@ mod tests { std::fs::remove_file(wordlist_path).unwrap(); } } + +