diff --git a/Cargo.toml b/Cargo.toml
index bb595ff..7a1c1c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,11 +20,11 @@ clap = { version = "4.6", features = ["derive"] }
tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
-reqwest = { version = "0.13", features = ["json", "cookies", "socks", "multipart", "form", "stream"] }
+reqwest = { version = "0.13", default-features = false, features = ["json", "cookies", "socks", "multipart", "form", "stream", "rustls-no-provider", "charset", "http2"] }
h2 = "0.4"
http = "1.4"
bytes = "1.11.1"
-tokio-rustls = "0.26" # used by exploit/scanner modules for target TLS connections
+tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] }
url = "2.5"
quick-xml = "0.39"
data-encoding = "2.10"
@@ -47,7 +47,7 @@ which = "8.0"
# FTP
suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
native-tls = "0.2"
-rustls = "0.23" # used by exploit/scanner modules for target TLS connections
+rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
rustls-pemfile = "2" # used by exploit/scanner modules
hyper = { version = "1", features = ["http1", "server"] }
hyper-util = { version = "0.1", features = ["tokio", "service"] }
@@ -66,9 +66,7 @@ rlimit = "0.11"
# Bluetooth
btleplug = "0.12"
-# TUI (WPair module)
-ratatui = "0.30"
-crossterm = "0.29"
+# WPair migrated from ratatui+crossterm TUI to rustyline REPL — deps removed.
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
@@ -86,7 +84,7 @@ serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
-axum = "0.8"
+axum = { version = "0.8", features = ["ws"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.23", features = ["v4", "serde"] }
@@ -104,6 +102,7 @@ once_cell = "1.21"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.35"
des = { version = "0.8.1", features = ["zeroize"] }
+zeroize = { version = "1", features = ["derive"] }
sha1 = "0.10"
strsim = "0.11"
ssh2 = "0.9.5"
@@ -131,8 +130,7 @@ walkdir = "2.5"
# Dependency overrides to address security advisories in transitive dependencies
# RUSTSEC-2026-0009: time >=0.3.47 fixes DoS via stack exhaustion (used by reqwest via cookie/cookie_store)
time = "0.3.47"
-# Note: paste (RUSTSEC-2024-0436, unmaintained) and lru (RUSTSEC-2026-0002, unsound) are
-# transitive warnings from ratatui 0.29 internals — no upstream fix available yet.
+# (ratatui 0.29 transitive advisories cleared when the TUI was replaced with rustyline.)
# ============================================
# Development profile: Fast incremental builds
diff --git a/README.md b/README.md
index f4b71a3..4fe83c0 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,9 @@
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
-
-
+
+
+
---
@@ -16,7 +17,7 @@ Full documentation lives in the **[Rustsploit Wiki](docs/Home.md)**. Below is a
| [Getting Started](docs/Getting-Started.md) | Installation, build, quick-start, Docker deployment |
| [Interactive Shell](docs/Interactive-Shell.md) | Shell walkthrough, command palette, chaining, shortcuts |
| [CLI Reference](docs/CLI-Reference.md) | Command-line flags, non-shell usage, output formats |
-| [API Server](docs/API-Server.md) | REST API startup, endpoints, auth, rate limiting, hardening |
+| [API Server](docs/API-Server.md) | REST + WebSocket API, PQ encryption, endpoints, rate limiting |
| [API Usage Examples](docs/API-Usage-Examples.md) | Practical curl workflows, request/response samples |
| [Module Catalog](docs/Module-Catalog.md) | All modules by category — exploits, scanners, creds |
| [Module Development](docs/Module-Development.md) | How to author new modules, lifecycle, dispatcher |
@@ -44,36 +45,39 @@ Full documentation lives in the **[Rustsploit Wiki](docs/Home.md)**. Below is a
- **Background jobs:** Run modules asynchronously with `run -j`, manage with `jobs` commands
- **Export/reporting:** Export all engagement data to JSON, CSV, or human-readable summary reports
- **Console logging:** `spool` command captures all output to file for documentation
-- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, MQTT, Fortinet — with IPv6 and TLS support
-- **Exploit coverage:** CVEs for GNU inetutils-telnetd, Apache Tomcat, TP-Link, Ivanti, Zabbix, OpenSSH, Jenkins, PAN-OS, Heartbleed, and more
-- **Scanners & utilities:** Port scanner, ping sweep, SSDP, HTTP title grabber, DNS recursion tester, directory bruteforcer, sequential fuzzer
-- **REST API server:** 30+ endpoints — authentication, rate limiting, IP tracking, full CRUD for credentials, hosts, services, loot, jobs
+- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, IMAP, RDP, RTSP, SNMP, L2TP, MQTT, VNC, MySQL, PostgreSQL, Redis, CouchDB, Elasticsearch, Memcached, HTTP Basic, Proxy, Fortinet — with IPv6 and TLS support
+- **Exploit coverage:** CVEs for VNC (LibVNC, TigerVNC, TightVNC, x11vnc), honeypots (Cowrie, Dionaea, HoneyTrap, SNARE), WAFs (SafeLine), Apache Camel, Kubernetes ingress-nginx, Commvault, MISP, Zimbra, Next.js, Vite, and 100+ more
+- **Scanners & utilities:** Port scanner, ping sweep, SSDP, HTTP title grabber, DNS recursion tester, directory bruteforcer, sequential fuzzer, proxy scanner, reflect scanner, vulnerability checker
+- **API server:** PQ-encrypted WebSocket transport — post-quantum cryptography, full CRUD for credentials, hosts, services, loot, jobs
+- **MCP server:** 38-tool Model Context Protocol server for AI-assisted pentesting via stdio
- **Plugin system:** Third-party modules via `src/modules/plugins/` with build-time discovery and startup safety warnings
-- **Security hardened:** Input validation, path traversal protection, honeypot detection, memory-safe operations
+- **Security hardened:** Input validation, path traversal protection, honeypot detection, root privilege checks, spool symlink protection, memory-safe operations
- **IPv4/IPv6 ready:** Both address families work out-of-the-box across all modules
---
## Quick Start
+**One command** (Debian/Ubuntu/Kali):
+
```bash
-# Install dependencies (Debian/Ubuntu/Kali)
-sudo apt update
-sudo apt install pkg-config libssl-dev rustc libdbus-1-dev freerdp2-x11
-
-# Install Rust
-curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-source $HOME/.cargo/env
-
-# Clone & build
-git clone https://github.com/s-b-repo/rustsploit.git
-cd rustsploit
-cargo build
-
-# Run
-cargo run
+sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev libdbus-1-dev cmake && (command -v cargo > /dev/null 2>&1 || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . "$HOME/.cargo/env")) && git clone https://github.com/s-b-repo/rustsploit.git && cd rustsploit && cargo run
```
+
+What each dependency does
+
+| Package | Required by | Why |
+|---------|------------|-----|
+| `build-essential` | Native crate compilation | gcc, make, libc headers |
+| `pkg-config` | `native-tls`, `ssh2` | Finds system libraries at build time |
+| `libssl-dev` | `native-tls`, `ssh2` | OpenSSL headers for TLS and SSH |
+| `libdbus-1-dev` | `btleplug` | D-Bus IPC for Bluetooth scanning |
+| `cmake` | `ssh2` (libssh2-sys) | Builds libssh2 from source |
+
+
+
+
For other distros (Arch, Gentoo, Fedora), Docker deployment, and one-liner installs, see **[Getting Started](docs/Getting-Started.md)**.
---
diff --git a/bugs.txt b/bugs.txt
deleted file mode 100644
index 85c04d9..0000000
--- a/bugs.txt
+++ /dev/null
@@ -1,8 +0,0 @@
---- API Initial Fuzzing Bugs ---
-BUG: /api/exec executes raw shell commands instead of mapping them
-
---- PortSwigger API Security Findings ---
-BUG (Mass Assignment/Error Handling): Server crashed on extra unexpected JSON fields (Status: 500)
-BUG (Lack of Resources/Rate Limiting): Server allowed 50 requests in 0.13 seconds with no rate limiting applied
-BUG (SSRF): API allows scanning internal/cloud metadata IP (169.254.169.254)
-INFO (SSRF): API legitimately allows scanning localhost/127.0.0.1 by its design.
diff --git a/build.rs b/build.rs
index 10f7d88..72e88eb 100644
--- a/build.rs
+++ b/build.rs
@@ -43,6 +43,14 @@ fn main() {
println!("cargo:rerun-if-changed=src/modules/{}", cat);
}
+ // Compile regexes once, reuse across all categories.
+ let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
+ .expect("hardcoded regex must compile");
+ let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo")
+ .expect("hardcoded regex must compile");
+ let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult")
+ .expect("hardcoded regex must compile");
+
// Generate a dispatcher for each category
let mut registry_entries: Vec = Vec::new();
@@ -52,7 +60,7 @@ fn main() {
let out_file = format!("{}_dispatch.rs", dispatch_name(cat));
let display_name = capitalize(cat);
- match generate_dispatch(&root, &out_file, &mod_prefix, &display_name) {
+ match generate_dispatch(&root, &out_file, &mod_prefix, &display_name, &run_re, &info_re, &check_re) {
Ok(_module_count) => {
registry_entries.push(RegistryEntry {
category: cat.clone(),
@@ -107,6 +115,9 @@ fn generate_dispatch(
out_file: &str,
mod_prefix: &str,
category_name: &str,
+ run_re: &Regex,
+ info_re: &Regex,
+ check_re: &Regex,
) -> Result> {
let out_dir = env::var("OUT_DIR").map_err(|_| "OUT_DIR environment variable not set")?;
let dest_path = Path::new(&out_dir).join(out_file);
@@ -116,7 +127,7 @@ fn generate_dispatch(
return Err(format!("Module directory '{}' does not exist", root).into());
}
- let mappings = find_modules(root_path)?;
+ let mappings = find_modules(root_path, run_re, info_re, check_re)?;
// Sort for deterministic output
let mut sorted_mappings: Vec<_> = mappings.into_iter().collect();
@@ -263,6 +274,29 @@ fn generate_dispatch(
}
writeln!(file, " _ => None,")?;
+ writeln!(file, " }}\n}}\n")?;
+
+ // === Check availability (no target needed) ===
+ writeln!(file, "/// Check if a module has a check() function without needing a target.")?;
+ writeln!(file, "pub fn check_available(module_name: &str) -> bool {{")?;
+ writeln!(file, " match module_name {{")?;
+
+ let mut check_avail_shorts: HashSet = HashSet::new();
+
+ for (key, _, caps) in &sorted_mappings {
+ if !caps.has_check { continue; }
+ let short_key = key.rsplit('/').next().unwrap_or(key);
+
+ if short_key == *key {
+ writeln!(file, r#" "{k}" => true,"#, k = key)?;
+ } else if check_avail_shorts.insert(short_key.to_string()) {
+ writeln!(file, r#" "{short}" | "{full}" => true,"#, short = short_key, full = key)?;
+ } else {
+ writeln!(file, r#" "{full}" => true,"#, full = key)?;
+ }
+ }
+
+ writeln!(file, " _ => false,")?;
writeln!(file, " }}\n}}")?;
let count = sorted_mappings.len();
@@ -348,18 +382,37 @@ fn generate_registry(entries: &[RegistryEntry]) -> Result<(), Box None,")?;
writeln!(f, " }}")?;
+ writeln!(f, "}}\n")?;
+
+ // Check availability (no target needed)
+ writeln!(f, "/// Check if a module has a check() function by category and module name.")?;
+ writeln!(f, "pub fn check_available_by_category(category: &str, module_name: &str) -> bool {{")?;
+ writeln!(f, " match category {{")?;
+ for e in entries {
+ writeln!(
+ f,
+ " \"{}\" => crate::commands::{}::check_available(module_name),",
+ e.category, e.dispatch_name
+ )?;
+ }
+ writeln!(f, " _ => false,")?;
+ writeln!(f, " }}")?;
writeln!(f, "}}")?;
Ok(())
}
+type ModuleMapping = (String, String, ModuleCapabilities);
+
/// Finds all valid modules recursively using WalkDir.
/// Returns (module_key, module_path, capabilities) tuples.
-fn find_modules(root: &Path) -> Result, Box> {
+fn find_modules(
+ root: &Path,
+ run_re: &Regex,
+ info_re: &Regex,
+ check_re: &Regex,
+) -> Result, Box> {
let mut mappings = HashSet::new();
- let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")?;
- let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo")?;
- let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult")?;
for entry in WalkDir::new(root).follow_links(false).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
@@ -372,11 +425,11 @@ fn find_modules(root: &Path) -> Result,
+ pub default_headers: Option,
+ }
+ pub fn build_http_client_with(timeout: Duration, opts: HttpClientOpts) -> Result { ... }
+ // existing build_http_client(timeout) stays as a thin wrapper over build_http_client_with(timeout,
+ Default::default()).
+
+ Then migrate all 50 modules — no diverging callers left in exploit code.
+
+ Gate: cargo build --release; re-run fire_all_modules.py, store diff vs Phase-A baseline. Timeout bucket should
+ shrink.
+
+ Phase C — Empirical bug triage (~1 session, after batch finishes)
+
+ Once /tmp/fire_results.tsv is complete, bucket by status:
+ 1. PANIC / unwrap()-in-net — fix all; replace with ? + anyhow::Context.
+ 2. IMMEDIATE_ERROR (<500 ms) — usually a hardcoded const or missing prompt default; fix inline.
+ 3. TIMEOUT_NO_READ — add protocol sniff + early-exit after first read:
+
+ let mut probe = [0u8; 8];
+ match tokio::time::timeout(Duration::from_millis(1500), stream.read(&mut probe)).await {
+ Ok(Ok(0)) | Ok(Err(_)) | Err(_) =>
+ return Ok(CheckResult::NotVulnerable("no banner".into())),
+ Ok(Ok(n)) if !looks_like_protocol(&probe[..n]) =>
+ return Ok(CheckResult::NotVulnerable("wrong protocol".into())),
+ _ => {}
+ }
+
+ Gate: PANIC bucket = 0, TIMEOUT bucket ≥50 % reduction.
+
+ Phase D — Feature additions (~2 sessions)
+
+ D1. Add pub async fn check() to the 114 missing modules. Priority = ~70 CVE-named ones. Template: reuse the
+ first TCP/HTTP probe from run(), return CheckResult::{Vulnerable|NotVulnerable|Unknown} without the payload.
+ For DoS floods where "check == exploit," omit with an inline comment.
+
+ D2. Replace hardcoded consts with cfg_prompt_* in ~73 modules. Keep current values as prompt defaults ⇒ zero
+ behavior change for existing callers; opens API/MCP overridability.
+
+ Gate: mcp__rustsploit__check_module returns a real result for every newly-checked module; list_options reflects
+ new knobs.
+
+ Phase E — Deep per-module audit of ALL 181 modules (rolling, multi-session)
+
+ User scope decision: full coverage. No deferral. Split into rolling session batches of ~20 modules each (≈9-10
+ sessions). Checklist tracked in src/modules/exploits/AUDIT.md (created once, updated per session) so progress
+ survives session boundaries.
+
+ Per-module checklist (applied uniformly):
+ 1. Payload correctness vs. upstream CVE PoC / advisory (link in doc-comment).
+ 2. Auth fallbacks where applicable (retry on 401/403 with alt credentials).
+ 3. Exponential backoff around retryable transient errors.
+ 4. Loot capture via crate::db::add_loot (shell output, extracted creds, files).
+ 5. Host/service registration via crate::db::add_host + add_service on successful fingerprint.
+ 6. Error context via anyhow::Context — every ? with human-readable .context(...).
+ 7. Doc block at top: CVE, vendor, affected versions, payload summary, references.
+ 8. ModuleRank honest w.r.t. reliability observed during testing.
+ 9. check() present (Phase D1 adds one if missing).
+ 10. cfg_prompt_* for every user-configurable knob (Phase D2 migrates consts).
+
+ Session batching order (priority first):
+ - E1 (session 1): highest-profile CVEs in network_infra/ (fortinet, ivanti, citrix, palo_alto, sonicwall) — 30
+ modules.
+ - E2: webapps RCE modules (craftcms, flowise, n8n, xwiki, roundcube, sharepoint, wordpress, sap, misp, mcpjam,
+ dify, langflow) — 25 modules.
+ - E3: frameworks (apache_tomcat, apache_camel, jenkins, nginx, php, wsus, http2) — 15 modules.
+ - E4: ssh module family (libssh_auth_bypass, asyncssh, paramiko, erlang_otp, sshpwn_*, libssh2_rogue_server,
+ openssh_regresshion, opensshserver_9_8p1race) — 15 modules.
+ - E5: router CVEs (tplink, ruijie, netgear, dlink, zte, zyxel, tenda, ubiquiti) — 25 modules.
+ - E6: vnc modules (13) + telnet (1) + voip (1) + cameras (6) — 21 modules.
+ - E7: dos modules (12 flood/amplification) — apply root-check + verify they behave cleanly.
+ - E8: honeytrap / snare / cowrie / dionaea / safeline — 15 modules.
+ - E9: crypto / ftp / ipmi / windows / bluetooth / payloadgens — 12 modules.
+ - E10: remaining + catch-all review.
+
+ Completion gate for each session: audit notes + diff committed, cargo build --release green, MCP smoke re-run.
+
+ Verification (cross-phase)
+
+ 1. cargo build --release must stay green after every phase.
+ 2. cargo clippy --all-targets -- -W clippy::all — no new warnings in touched files.
+ 3. python3 /tmp/fire_all_modules.py smoke run between phases; /tmp/fire_results.tsv compared against the
+ pre-phase snapshot for regressions.
+ 4. MCP smoke: mcp__rustsploit__list_modules, module_info on every touched module, check_module on all Phase-D1
+ additions, run_module on 3 representative modules (ssh, HTTP RCE, DoS).
+ 5. CLI smoke: run the shell (cargo run -- -s), use , check, verify prompts are satisfied via set
+ and no io::stdin blocks.
+
+ Decisions Locked
+
+ - Phase E scope → all 181 modules, batched across rolling sessions (E1–E10, ~20 modules each), checklist
+ tracked in src/modules/exploits/AUDIT.md.
+ - build_http_client extension → new HttpClientOpts struct + build_http_client_with(timeout, opts); existing
+ build_http_client(timeout) stays as wrapper. All 50 exploit modules migrate; no diverging callers.
+ - Raw-socket modules → shared crate::utils::require_root(&str) helper (new src/utils/privilege.rs); every
+ flood/amplification/raw-sniff module gates at top of run() returning anyhow::Error, no crash.
+
+
+ Completed
+ - ✅ Phase A1: thread::sleep → tokio::time::sleep (2 files — other sites were already correct inside
+ spawn_blocking/native threads)
+ - ✅ Phase A2: Populated all 39 empty references: vec![] with category-appropriate refs
+ - ✅ Phase A3: New require_root() helper in src/utils/privilege.rs wired into 9 raw-socket modules
+ - ✅ Phase B2a: HttpClientOpts + build_http_client_with() added to src/utils/network.rs; old build_http_client()
+ now wraps it
+
+
+
+ ┌─────────────────────────────────────┬───────────┬──────────────────────────────────────────────────────────┐
+ │ Phase │ Status │ Impact │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ A1 — thread::sleep → tokio::sleep │ ✅ done │ 2 SSH modules (14 sites) │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ A2 — Populate empty references │ ✅ done │ All 39 modules │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ A3 — require_root helper │ ✅ done │ New src/utils/privilege.rs + 9 raw-socket modules │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ B2a — Extend build_http_client │ ✅ done │ HttpClientOpts + build_http_client_with │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ B1 — TcpStream::connect migration │ ✅ │ ~14 sites / 10 files migrated; complex patterns deferred │
+ │ │ partial │ to E │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ B2b — reqwest migration │ ✅ done │ 47 sites / 41 files unified │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ C — Empirical batch triage │ ⏳ │ Needs fresh /tmp/fire_results.tsv │
+ │ │ pending │ │
+][
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ D2 — cfg_prompt_* for hardcoded │ ⏳ │ 73 modules │
+ │ consts │ pending │ │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ E — Deep audit all 181 modules │ ⏳ │ rolling E1–E10 │
+ │ │ pending │ │
+ └─────────────────────────────────────┴───────────┴──────────────────────────────────────────────────────────┘
+
+ MCP Server Protocol Safety (src/mcp/server.rs):
+ - Added isolate_protocol_stdout() — redirects fd 1 to /dev/null so stray println! from modules can't corrupt the
+ JSON-RPC stream
+ - Added MAX_LINE_BYTES (1 MiB) cap to prevent memory exhaustion from malicious input
+ - Switched to byte-level read_until for binary safety (vs read_line which requires valid UTF-8)
+ - Explicit non-UTF-8 error handling with proper JSON-RPC error response
+ - Made write_response accept dyn AsyncWrite for flexibility
+ - Fixed MCP spec compliance: resources/read now wraps response in { "contents": [...] }
+
+ Spool Security (src/spool.rs):
+ - Added O_NOFOLLOW flag to prevent TOCTOU race condition on symlinks
+ - Lock-first pattern: acquires the write lock before creating the file to prevent orphaned files
+ - Parent directory symlink check (Bug #96): blocks spool through symlinked parent dirs
+ - write_line() now returns Result<(), std::io::Error> with flush after every write (Bug #11)
+ - Proper error logging in start_at_path() and stop()
+
+ Output Error Handling (src/output.rs):
+ - Added SPOOL_WARNED pattern for spool write error suppression (logs once, then suppresses)
+ - _mprint_line and _mprint_newline now properly handle write_line Result
+
+ Shell (src/shell.rs):
+ - Proper error handling for spool write_line call
+
+ Build System (build.rs):
+ - Regex caching: compiles 3 regexes once in main() and reuses across all category dispatchers
+ - Fast pre-filter: content.contains("fn run") skip before regex matching
+ - Added check_available() function generation per category
+ - Added check_available_by_category() in unified registry
+
+ API SSRF Protection (src/api.rs):
+ - Restored comprehensive SSRF blocking: cloud metadata endpoints, link-local ranges, IPv6 metadata, wildcard DNS
+ services
+ - Added is_blocked_ip() function for IP-level blocking
+ - Made is_blocked_target() pub(crate) for MCP tools access
+ - Added () and {} to shell metacharacter checks
+
+ MCP Tools Input Validation (src/mcp/tools.rs):
+ - Added SSRF checks to handle_run_module and handle_check_module
+ - Added field length limits (4096 chars) and control character validation to handle_add_cred
+ - Added IP/hostname validation (256 chars) to handle_add_host
+ - Added port range validation (port 0 blocked) and field validation to handle_add_service
+
+
+
+ Warnings fixed (10 → 0):
+ - Removed unused extract_ip_from_target re-export from utils/mod.rs
+ - Wired categories() and has_check() into commands/mod.rs, called from ws.rs rpc_list_modules_enriched — this uses the generated CATEGORIES, check_available_by_category, and all 4 per-dispatch check_available functions
+ - Removed 9 unused ModuleConfig fields (port, username_wordlist, password_wordlist, concurrency, stop_on_success, save_results, output_file, verbose, combo_mode) — only custom_prompts and api_mode were ever read
+ - Wired results_dir() from config.rs into ws.rs instead of hardcoded "results" path
+
+ Slop audit result: Only 4 .expect() calls in the entire codebase, all on infallible operations (hardcoded regex, fixed-size byte slice, in-memory zlib). No .unwrap() calls on fallible paths.
+
+
+ Completed fixes:
+ - P0 (SSRF): Fixes #1-3 — async DNS resolution checks in ws.rs and mcp/tools.rs
+ - P1 (Race/Validation): Fix #4 (port truncation), #5 (WS connection limit TOCTOU), #6 (job spawn TOCTOU), #7
+ (session expiry/cleanup), #8 (file permission TOCTOU)
+ - P2 (Hardening): Fix #9 (health info disclosure), #10 (secret zeroization — Zeroizing> on all chain keys,
+ root keys, IKM), #11 (subscription limit), #12 (serialization error propagation), #13 (per-IP handshake rate
+ limiting — 10/min/IP with 429 response), #14 (job ID collision check)
+ - P3 (Low): Fix #15 (cred_type validation), #16 (removed is_locked() stub)
+
+First task to need a prompt (e.g., "Port Range") locks the shared cache, prompts the user once, caches the answer
+All other tasks find the cached answer instantly — zero stdin reads
+Single-target mode is completely unaffected (no cache active)
+Files changed
+File Change
+
+context.rs
+Added PromptCache type + prompt_cache field on RunContext
+
+prompt.rs
+Added cached_prompt() helper, updated all 7 cfg_prompt_* functions
+
+commands/mod.rs
+Wrapped all 3 concurrent dispatch paths (mass scan, file, CIDR) with shared cache via RUN_CONTEXT.scope()
+You can test it with cargo run and try a mass scan target like 0.0.0.0/0 — each prompt should now appear exactly once.
+ Fixed (7 changes across 8 files):
+
+ 1. TOCTOU file permissions (cred_store.rs, loot.rs, global_options.rs, pq_channel.rs): Files now created with
+ 0o600 atomically via OpenOptions::mode() instead of write-then-chmod.
+ 2. Path traversal (creds/utils.rs): mark_ip_checked and is_ip_checked now reject absolute paths, paths with
+ separators, and null bytes - not just ...
+ 3. SSH timeout hardening (ssh_bruteforce.rs): Added set_read_timeout, set_write_timeout, and sess.set_timeout() so
+ handshake/auth can't hang a blocking thread forever.
+ 4. Combo overflow protection (creds/utils.rs): Vec::with_capacity now caps pre-allocation at 10M entries to avoid
+ OOM on huge cross products; Vec still grows organically beyond that.
+ 5. Atomic ordering (creds/utils.rs): print_progress and print_final now use Ordering::Acquire instead of Relaxed
+ for consistent stats.
+ 6. Streaming wordlist support (utils/modules.rs, creds/utils.rs): Added load_lines_uncapped, load_lines_batched,
+ file_size, STREAMING_THRESHOLD (250 MB), and run_bruteforce_streaming - processes huge wordlists in 500K-line
+ batches without materializing the full cross product.
+
+
+ 11 files updated:
+
+ 1. docs/Module-Catalog.md — Added 57 new module entries (45 exploits, 3 scanners, 10 creds), 6 new category
+ sections (Cowrie, Dionaea, HoneyTrap, SafeLine, Snare, VNC), updated totals to 239
+ 2. docs/Changelog.md — New April 2026 section with all changes, updated module totals
+ 3. README.md — Updated counts, highlights (credential protocols, exploit categories, MCP server, WebSocket),
+ scanner list
+ 4. docs/Home.md — Updated module counts in wiki index (239 total)
+ 5. docs/Module-Development.md — Added mcp/, ws.rs, privilege.rs to layout tree, updated module counts
+ 6. docs/Utilities-Helpers.md — Added network utils and privilege check sections with full API docs
+ 7. docs/API-Server.md — Added WebSocket endpoint /pq/ws, transport documentation, updated endpoint count
+ 8. docs/CLI-Reference.md — Added --mcp flag
+ 9. docs/Security-Validation.md — Added MCP, spool, privilege check security patterns
+ 10. bugs.txt — Cleaned up debug logs, replaced with resolved bug list
+ 11. changelogs/changelog-latest.md — Prepended full session changelog
+
+ Mass scan prompt spam fix (context.rs, utils/mod.rs, utils/prompt.rs, commands/mod.rs):
+ - Added a global BATCH_ACTIVE flag and BATCH_CACHE that don't rely on tokio::task_local propagation into spawned
+ tasks
+ - is_batch_mode() now checks the global flag first, falling back to the task-local
+ - cached_prompt() now uses the global batch cache when active, ensuring only one prompt per key even across
+ tokio::spawn boundaries
+ - All three concurrent dispatch paths (random, file, CIDR) set set_batch_mode(true) before spawning and
+ set_batch_mode(false) after completion
+
+ hex.rs (src/native/hex.rs):
+ - Added decode() function — accepts upper/lowercase hex, returns Vec
+ - Added DecodeError enum with OddLength and InvalidChar variants, implementing Display and Error
+ - No test code — production functions only
+ v
+
+ New module: src/modules/exploits/dos/telnet_iac_flood.rs
+
+ Four attack modes that exploit the unbounded IAC parsing bug:
+
+ 1. WILL/DO Storm — Rapid-fire WILL/DO/WONT/DONT for 14 options, forcing the target to generate and process response packets for each
+ 2. Subnegotiation Bomb — Sends IAC SB frames without IAC SE terminators, plus 4000+ byte payloads that force parsers to scan the entire buffer looking for a terminator that never comes
+ 3. NOP Interleave — 1000+ IAC NOP commands interleaved with WILL/DO, forcing per-byte state machine transitions
+ 4. Combined — All three plus rapid option toggling (WILL→WONT→DO→DONT cycles)
+
+ Fea
+
+ Fixed (7 changes across 8 files):
+
+ 1. TOCTOU file permissions (cred_store.rs, loot.rs, global_options.rs, pq_channel.rs): Files now created with 0o600 atomically via OpenOptions::mode() instead of write-then-chmod.
+ 2. Path traversal (creds/utils.rs): mark_ip_checked and is_ip_checked now reject absolute paths, paths with separators, and null bytes - not just ...
+ 3. SSH timeout hardening (ssh_bruteforce.rs): Added set_read_timeout, set_write_timeout, and sess.set_timeout() so handshake/auth can't hang a blocking thread forever.
+ 4. Combo overflow protection (creds/utils.rs): Vec::with_capacity now caps pre-allocation at 10M entries to avoid OOM on huge cross products; Vec still grows organically beyond that.
+ 5. Atomic ordering (creds/utils.rs): print_progress and print_final now use Ordering::Acquire instead of Relaxed for consistent stats.
+ 6. Streaming wordlist support (utils/modules.rs, creds/utils.rs): Added load_lines_uncapped, load_lines_batched, file_size, STREAMING_THRESHOLD (250 MB), and run_bruteforce_streaming - processes huge wordlists in 500K-line batches
+ without materializing the full cross product.
+
- Terminal 2 — GUI Backend:
- cd ~/Pictures/Jenkins/rustsploit-thekiaboys/rustsploit\ gui/backend && npm run dev
+ telnet_auth_bypass_cve_2026_24061.rs (line 207):
+ - while i < data.len() - 1 → while i + 1 < data.len() — eliminates the usize underflow when data is empty (would wrap to usize::MAX)
+ - Added sb_data.len() < MAX_SB_DATA (4096 cap) — prevents unbounded memory growth from a malicious server sending endless SB data without SE
- Terminal 3 — Frontend:
- cd ~/Pictures/Jenkins/rustsploit-thekiaboys/rustsploit\ gui/frontend && npm run dev
+ telnet_bruteforce.rs (line 1097):
+ - Added sb_limit = (i + 4096).min(data.len()) — caps the SB scan at 4096 bytes past the SB start, so a malicious server sending SB without SE can't force the parser to process the entire buffer repeatedly
+
+ Bug 1 — set_batch_mode(true) leaks on early ? return (file path, line 319-320)
+
+ crate::context::set_batch_mode(true); // line 318
+ let content = ...safe_read_to_string_async(target, None).await
+ .with_context(|| ...)?; // line 320 — early return!
+
+ If the file read fails, ? returns Err and set_batch_mode(false) on line 390 is never reached. Every subsequent
+ single-target run will think it's in batch mode — headers suppressed, prompts cached from a stale previous run.
+ Same issue in the random path at line 243 (semaphore.acquire_owned().await?).
+
+ Bug 2 — try_lock() silently skips cache clear (context.rs line 35)
+
+ if let Ok(mut guard) = BATCH_CACHE.try_lock() {
+ guard.clear();
+ }
+
+ try_lock() on a tokio::sync::Mutex fails if another task still holds it (e.g. a prompt awaiting stdin from a
+ previous run that was Ctrl+C'd). The cache retains stale answers from the prior scan — a different module's
+ prompts could silently get wrong defaults.
+
+ Bug 3 — Global state conflicts with concurrent API dispatches
+
+ BATCH_ACTIVE is a single global bool. If the WebSocket API dispatches two modules concurrently (one mass scan, one
+ single-target), the single-target run sees is_batch_active() == true and suppresses its headers/caches its
+ prompts incorrectly. The CLI is single-threaded so this won't bite today, but the API path uses RUN_CONTEXT
+ precisely to avoid global state.
+
+
+ all improvements made to telnet_bruteforce.rs:
+
+ Security hardening:
+ - MAX_IAC_ROUNDS (64) — caps total IAC negotiation responses per drain call, prevents infinite WILL/DO cycling
+ from malicious servers
+ - MAX_DRAIN_BYTES (64KB) — caps total bytes read per drain call, prevents memory exhaustion
+ - IAC response batching — concatenates all responses into a single write_all instead of N separate writes,
+ reducing syscalls
+
+ Protocol improvements:
+ - TERMINAL_SPEED subnegotiation — responds with 38400,38400 instead of refusing
+ - NEW_ENVIRON/ENVIRON subnegotiation — sends empty environment response instead of refusing
+ - Accepted TERMINAL_SPEED and ENVIRON in negotiate_do() (enables WILL before server sends SB)
+ - No-auth device detection — if banner shows a shell prompt with no login prompt, attempts verification
+ immediately
+
+ Effectiveness improvements:
+ - Banner fingerprinting (DeviceType enum with 15 device types) — detects Dahua, Xiongmai, HiSilicon, ZTE, Huawei,
+ MikroTik, Ubiquiti, Cisco, D-Link, TP-Link, Netgear, BusyBox, Raspberry Pi, Dell iDRAC, HP printers
+ - Device-specific credential prioritization — tries known device-specific defaults before generic credentials in
+ mass scan mode
+ - ~40 additional default credentials (95+ total) — Ubiquiti, Raspberry Pi, Dell iDRAC, HP printers, Zyxel, Alpine
+ Linux, Synology NAS, Cisco small business, Aerohive, Symbol/Zebra, etc.
+ - Connection pre-check — verifies at least one port is reachable before loading wordlists (single target mode)
+
+ Accuracy improvements:
+ - Multi-probe shell verification — tries echo _RS_VERIFIED_, then id (checks for uid=), then structural prompt
+ analysis
+ - 6 more false-positive rejection patterns in looks_like_shell_prompt()
+ - 7 more failure indicators (Japanese, Korean, additional English patterns)
+ - 5 more success indicators (EdgeOS, RouterOS, IoT menu patterns)
+ - 6 more login prompt patterns (Japanese, NVR/router/switch/modem-specific)
+
+ Code quality:
+ - collect_chunk_result() helper — deduplicates 3 copies of the streaming chunk result processing pattern
+ - grab_banner() function — clean banner-only connection for fingerprinting
+
+
+
+ Critical - Unbounded network reads:
+ - vnc/tightvnc_ft_path_traversal.rs - Added 16MB cap on FT message size and 64MB cap on total download with
+ per-chunk validation
+
+ Critical - Integer overflow:
+ - ssh/libssh2_rogue_server.rs - Fixed off + 4 + len overflow using saturating_add()
+
+ High - Unbounded SSH channel reads:
+ - ssh/sshpwn_session.rs - Truncate stdout/stderr to 10MB + added 500MB file upload size check
+ - ssh/sshpwn_scp_attacks.rs - Truncate stdout/stderr to 10MB
+ - ssh/sshpwn_pam.rs - Truncate env output to 10MB
+ - ssh/asyncssh_beginauthpass.rs - Truncate output to 10MB
+ - ssh/paramiko_authnonepass.rs - Truncate output to 10MB
+ - ssh/paramiko_unknown_method.rs - Truncate output to 10MB
+ - ssh/sshpwn_sftp_attacks.rs - Cap SFTP read to 10MB via Read::take()
+
+ High - Decompression bomb:
+ - mongo/mongobleed.rs - Cap zlib decompression to 64MB via Read::take()
+
+ Medium - File read without size check:
+ - payloadgens/payload_encoder.rs - Added 100MB file size check before encoding
+
+ Low - Capacity overflow prevention:
+ - native/hex.rs - saturating_mul(2) on capacity
+ - native/payload_engine.rs - saturating_mul on 3 capacity calculations
+ - utils/sanitize.rs - saturating_mul(2) on 2 shell escape capacities
+
+ Bonus - Edition compatibility:
+ - context.rs - Renamed gen parameter (reserved keyword in Rust 2024)
+
+
+ 1. Batch mode leak on early ? return — fixed with RAII BatchGuard that calls set_batch_mode(false) on drop, even
+ on error returns or panics
+ 2. try_lock() silently skipping cache clear — replaced with a lock-free generation counter. enter_batch_mode()
+ bumps the generation; cached_prompt() lazily clears stale entries when it sees a generation mismatch while already
+ holding the lock
+ 3. blocking_lock() in async context — eliminated entirely, now lock-free
+ 4. gen reserved keyword — renamed to batch_gen (Rust 2024 edition)
+
+ Bug 3 (global state vs concurrent API) remains by design — the global batch mode is a CLI-only fallback. API
+ requests use RUN_CONTEXT task-local with per-request custom_prompts, which short-circuits before cached_prompt is
+ ever reached. No functional conflict.
+Security & Bug Audit: RustSploit
+
+ Context
+
+ Full security and bug audit of the RustSploit penetration testing framework. The codebase has strong Rust
+ fundamentals (memory safety, type system) and solid crypto (post-quantum ML-KEM + X25519). The issues below are
+ real, actionable findings organized by severity.
+
+ ---
+ CRITICAL / HIGH Severity
+
+ 1. TOCTOU Race in File Permission Setting
+
+ Files: src/cred_store.rs:165-191, src/loot.rs:108-112, src/global_options.rs:88-114, src/pq_channel.rs:68-89
+
+ Bug: Files are created world-readable, then permissions set to 0o600 in a separate call. Between write() and
+ set_permissions(), a local attacker can read credentials or private keys.
+
+ Fix: Use OpenOptionsExt::mode(0o600) to set permissions atomically at creation:
+ use std::os::unix::fs::OpenOptionsExt;
+ tokio::fs::OpenOptions::new()
+ .write(true).create(true).truncate(true)
+ .mode(0o600)
+ .open(&tmp).await?
+ All 4 files follow the same pattern - fix should be a shared utility.
+
+ ---
+ 2. Path Traversal in State File Handling
+
+ File: src/modules/creds/utils.rs:300-314 (mark_ip_checked())
+
+ Bug: Validates state_file by checking for .. substring, but does not block:
+ - Absolute paths (/etc/passwd passes)
+ - Symlink traversal
+ - Double encoding
+
+ Fix: Canonicalize the path and verify it's within an allowed directory.
+
+ ---
+ 3. Credential Plaintext Logging
+
+ Files: All bruteforce modules (ssh, mysql, http_basic, ftp, rdp, redis, etc.)
+
+ Bug: Found credentials are printed to stdout including the password in cleartext:
+ crate::mprintln!("\r{}", format!("[+] {} -> {}:{}", display, user, pass).green().bold());
+ Also stored in result files without zeroization.
+
+ Fix:
+ - Mask passwords in output (show first/last char or hash)
+ - Use zeroize crate for credential strings after use
+ - Warn users about terminal history exposure
+
+ ---
+ 4. DNS Resolution Without Timeout
+
+ File: src/modules/creds/generic/ssh_bruteforce.rs:344-346
+
+ Bug: to_socket_addrs() is a blocking DNS call with no timeout. A malicious or slow DNS server hangs the bruteforce
+ indefinitely.
+
+ Fix: Wrap DNS resolution in tokio::time::timeout(Duration::from_secs(5), ...).
+
+ ---
+ 5. Blocking Operations in Async Context
+
+ File: src/modules/creds/generic/ssh_bruteforce.rs:343-362
+
+ Bug: Inside spawn_blocking, sess.handshake() can hang indefinitely. The outer timeout wraps the JoinHandle, not
+ the inner blocking operation, so a hung handshake ties up a tokio blocking thread permanently.
+
+ Fix: Set TCP-level timeouts on the socket before passing to libssh2, or use a signal mechanism to abort the
+ blocking task.
+
+ ---
+ 6. Combo Generation Integer Overflow
+
+ File: src/modules/creds/utils.rs:647-658
+
+ Bug: Vec::with_capacity(usernames.len() * passwords.len()) can overflow on large wordlists (1M x 1M = 1 trillion
+ entries), causing OOM or panic.
+
+ Fix: Use checked_mul() and enforce a maximum combination count.
+
+ ---
+ MEDIUM Severity
+
+ 7. No Per-Second Rate Limiting in Mass Scan
+
+ File: src/modules/creds/utils.rs:381-615
+
+ Bug: Concurrency semaphore limits parallel tasks, but with fast probes and high concurrency (e.g., 1000), this
+ produces 100k+ packets/sec - effectively a DoS. No delay between retries either.
+
+ Fix: Add configurable requests_per_second throttle and retry delay.
+
+ ---
+ 8. Inconsistent Input Validation in MCP Tools
+
+ File: src/mcp/tools.rs (lines 595-612, 648-662, 691-698)
+
+ Bug: Different handlers have different length limits (host: 4096, IP: 256). IP field doesn't validate format. No
+ shared validation constants.
+
+ Fix: Create shared validation constants and utility functions.
+
+ ---
+ 9. Config Lock Poisoning Unrecoverable
+
+ File: src/config.rs (lines 63, 77, 115, 133, 184, 194, 199)
+
+ Bug: RwLock poison errors are caught but never recovered. A single panic in a locked section makes the entire
+ config system permanently broken.
+
+ Fix: Either use parking_lot::RwLock (which doesn't poison) or add recovery logic.
+
+ ---
+ 10. Race Condition in Progress Stats
+
+ File: src/modules/creds/utils.rs:128-175
+
+ Bug: Uses Ordering::Relaxed for all atomic loads in print_progress(). Stats can be inconsistent (e.g., showing
+ more successes than total attempts).
+
+ Fix: Use Ordering::Acquire for consistency.
+
+ ---
+ 11. Telnet IAC Parser Infinite Loop Risk
+
+ File: src/modules/creds/generic/telnet_bruteforce.rs
+
+ Bug: IAC negotiation has no limits on SB (subnegotiation) frame size or iteration count. A malicious server can
+ send endless IAC sequences.
+
+ Fix: Add max frame size and iteration limits.
+
+ ---
+ 12. Transitive Dependency Vulnerabilities
+
+ File: Cargo.toml:135-136
+
+ Bug: paste (RUSTSEC-2024-0436, unmaintained) and lru (RUSTSEC-2026-0002, unsound) via ratatui 0.29. No upstream
+ fix yet.
+
+ Fix: Monitor for ratatui update; consider alternative TUI library if unresolved.
+
+ ---
+ LOW Severity
+
+ 13. Unsafe errno Access Without Validation
+
+ File: src/mcp/server.rs:12-31
+
+ Direct *libc::__errno_location() dereference. Consider using errno crate.
+
+ 14. Session Cleanup - No Fallback
+
+ File: src/api.rs:223-242
+
+ Cleanup task runs every 300s. If it panics, no fallback exists. Already has max session limit elsewhere, so impact
+ is low.
+
+ 15. File Export Path Assumes CWD Safety
+
+ File: src/export.rs:214-228
+
+ Validates filename but writes to CWD. In API mode, CWD should be operator-controlled. Document this assumption.
+
+ 16. Redis "No Auth" False Positive
+
+ File: src/modules/creds/generic/redis_bruteforce.rs:178-196
+
+ PING succeeding without auth doesn't mean Redis is fully unauthenticated. Should test with INFO or SELECT.
+
+ 17. Partial File Write on Error
+
+ File: src/modules/creds/generic/ssh_bruteforce.rs:288-327
+
+ If writeln! fails partway through writing results, the file is left in partial state. Use temp file + atomic
+ rename.
+
+ 18. Regex Compiled in Loop
+
+ File: src/modules/exploits/crypto/heartbleed.rs:325-407
+
+ Regex patterns compiled per iteration. Pre-compile at module load time for performance.
+
+ ---
+ Already Implemented (Previous Rounds)
+
+ Items 1 (TOCTOU), 2 (path traversal), 4 (DNS timeout), 5 (blocking ops), 6 (combo overflow), 10 (atomic ordering)
+ — all fixed. Streaming wordlist support added. Edge case hardening applied across SSH/VNC/MongoDB/payload modules.
+
+ ---
+ Phase 3: Scanner Module Fixes
+
+ Context
+
+ Scanner modules audit revealed ~15 actionable bugs across 10+ scanner files. No shared scanner utility exists —
+ each module independently implements buffer handling and timeout logic, causing inconsistency and gaps.
+
+ Fix Order (by severity)
+
+ S1. CIDR /0 Expansion OOM — src/modules/scanners/ping_sweep.rs:354-366
+
+ Bug: host_set collects all IPs from CIDR with no limit. A /0 network = 4 billion IPs → OOM.
+ Fix: Before iterating, check net.size(). If > 1M hosts, warn and require confirmation. Use .take(limit) as safety
+ net.
+ const MAX_CIDR_HOSTS: usize = 1_048_576; // 1M = /12 IPv4
+ for net in &config.targets {
+ let size = match net.size() {
+ ipnetwork::NetworkSize::V4(n) => n as u128,
+ ipnetwork::NetworkSize::V6(n) => n,
+ };
+ if size > MAX_CIDR_HOSTS as u128 {
+ crate::mprintln!("CIDR {} has {} hosts (max {}), skipping", net, size, MAX_CIDR_HOSTS);
+ continue;
+ }
+ for host in net.iter() { host_set.insert(host); }
+ }
+
+ S2. Duplicate HTTP Methods — src/modules/scanners/api_endpoint_scanner.rs:155-158
+
+ Bug: Extended methods list has exact duplicate line, adding PUT/PATCH/HEAD/OPTIONS/CONNECT/TRACE twice.
+ Fix: Remove duplicate line 157. Trivial one-line fix.
+
+ S3. Hardcoded DNS Timeout — src/modules/scanners/dns_recursion.rs:176-181
+
+ Bug: Uses Duration::from_secs(5) instead of user-configured timeout_secs. Display message also hardcoded.
+ Fix: Replace 5 with timeout_secs in both the timeout call and the print statement.
+
+ S4. Unbounded HTTP Response Body — src/modules/scanners/http_title_scanner.rs:196-207
+
+ Bug: Comment says "at most 256KB" but response.bytes().await downloads full body before truncation.
+ Fix: Use bytes_stream() with early termination at 256KB:
+ use futures_util::StreamExt;
+ let mut buffer = Vec::with_capacity(256 * 1024);
+ let mut stream = response.bytes_stream();
+ while let Some(chunk) = stream.next().await {
+ let chunk = chunk.context("Stream read error")?;
+ buffer.extend_from_slice(&chunk);
+ if buffer.len() >= 256 * 1024 { buffer.truncate(256 * 1024); break; }
+ }
+
+ S5. Response Not Consumed — src/modules/scanners/http_method_scanner.rs:139-174
+
+ Bug: Response object created but body never consumed/dropped explicitly. Reqwest buffers body in memory.
+ Fix: Drop response explicitly after extracting status, or read a small portion:
+ let status = resp.status();
+ drop(resp); // release connection and buffered body
+
+ S6. Port 0 Acceptance — src/modules/scanners/port_scanner.rs:146 and ping_sweep.rs:1004-1020
+
+ Bug: Source port allows 0 in port_scanner. Port parsing in ping_sweep accepts port 0.
+ Fix: In parse_ports(), skip port 0 with warning. In port_scanner, validate source port > 0.
+
+ S7. Semaphore Acquisition Pattern — src/modules/scanners/port_scanner.rs:258-259
+
+ Bug: Semaphore acquired before spawning task. If acquire fails, loop exits leaving orphan tasks.
+ Fix: Move semaphore acquisition inside spawned task.
+
+ S8. Wildcard DNS Detection — src/modules/scanners/subdomain_scanner.rs:139-150
+
+ Bug: Tests only ONE random subdomain for wildcard. False negatives from slow DNS or collisions.
+ Fix: Test 3 random subdomains. Mark wildcard only if at least 2 resolve to same IP.
+
+ S9. Inconsistent Banner Buffers — src/modules/scanners/service_scanner.rs (multiple)
+
+ Bug: Banner buffers range from 512B to 8192B across probes with no consistency.
+ Fix: Define const MAX_BANNER: usize = 4096; and use it across all probes. Not a security issue (single .read() is
+ bounded by buffer size) but inconsistent behavior.
+
+ S10. Path Traversal in Output Files — src/modules/scanners/sequential_fuzzer.rs:368-371
+
+ Bug: Output directory built from timestamp (safe) but template-derived file names could escape.
+ Fix: Sanitize output filenames — strip directory separators, .., null bytes.
+
+ S11. Redis Scanner Silent Truncation — src/modules/scanners/redis_scanner.rs:52-63
+
+ Bug: Fixed 8KB buffer for Redis responses. Large INFO responses silently truncated.
+ Fix: Accept truncation (this is a scanner, not a data extractor) but log when truncation occurs.
+
+ S12. SSH Scanner Blocking Connect — src/modules/scanners/ssh_scanner.rs:140
+
+ Bug: blocking_tcp_connect is called with timeout but inside spawn_blocking. If the underlying OS doesn't honor the
+ timeout, the thread hangs.
+ Fix: Set TcpStream::connect_timeout() explicitly and add read/write timeouts on the stream.
+
+ ---
+ Verification Plan
+
+ 1. cargo build — must compile cleanly
+ 2. cargo clippy — no new warnings
+ 3. Test CIDR expansion: verify /8 and larger produce warnings, /24 works normally
+ 4. Test api_endpoint_scanner: verify no duplicate methods in scan output
+ 5. Test dns_recursion: verify custom timeout applies
+ 6. Test port parsing: verify port 0 rejected with message
+ 7. General: cargo test passes
+
+ ---
+ Implementation Notes
+
+ - Files to modify: 10 scanner module files
+ - No new dependencies required — futures_util::StreamExt already available via reqwest
+ - No shared utility file needed — fixes are localized per-module
+ - Estimated changes: ~150 lines across all files
+
+
+ S1. CIDR OOM (ping_sweep.rs) — Networks larger than 1M hosts are now rejected with a warning instead of OOM'ing
+ the process.
+
+ S2. Duplicate methods (api_endpoint_scanner.rs) — Removed the duplicate line that added
+ PUT/PATCH/HEAD/OPTIONS/CONNECT/TRACE twice.
+
+ S2b. Silent error handling (api_endpoint_scanner.rs) — DEBUG method parse failure now logs a warning instead of
+ silently substituting GET.
+
+ S3. Hardcoded DNS timeout (dns_recursion.rs) — Added configurable timeout prompt (default 5s). Both the query
+ timeout and display message now use the user's value.
+
+ S4. Unbounded HTTP body (http_title_scanner.rs) — Replaced response.bytes().await (downloads entire body) with
+ bytes_stream() that terminates at 256KB.
+
+ S5. Response leak (http_method_scanner.rs) — Response is now explicitly dropped after extracting status to release
+ buffered body memory.
+
+ S6. Port 0 (ping_sweep.rs, port_scanner.rs) — Port 0 is now skipped with a warning in parse_ports(). Source port
+ range changed from 0-65535 to 1-65535.
+
+ S8. Wildcard DNS (subdomain_scanner.rs) — Tests 3 random subdomains instead of 1. Requires 2+ matches to declare
+ wildcard, reducing false negatives.
+
+ S11. Redis truncation (redis_scanner.rs) — Logs a message when the 8KB buffer is fully consumed, indicating
+ possible response truncation.
+
+
+ - One command — installs system deps, installs Rust (skips if already present via command -v cargo), clones, and
+ runs
+ - Removed rustc from apt — the system rustc package is typically too old for edition 2024; rustup handles this
+ properly
+ - Added build-essential — needed for gcc/make to compile native crate extensions
+ - Added cmake — needed by libssh2-sys to build libssh2 from source for the ssh2 crate
+ - Moved freerdp2-x11 to optional — it's only used by the RDP module which shells out to xfreerdp
+ - Collapsed cargo build + cargo run into just cargo run — it builds automatically if needed
+ - Added a collapsible table explaining what each dependency is for
+
+Removed. The RDP module uses a native Rust implementation (src/native/rdp.rs) — no external freerdp2-x11 or
+ rdesktop needed.
+
+All doc fixes applied. Here's what was fixed across 8 files:
+
+ Getting-Started.md:
+ - Removed freerdp, rustc from all 4 OS install commands (native RDP, use rustup)
+ - Added correct deps: build-essential, cmake, libdbus-1-dev
+ - Changed "REST API server" to "WebSocket API server"
+ - Updated minimum Rust version to "1.85+ (edition 2024)"
+
+ README.md:
+ - Fixed "REST + WebSocket" to "WebSocket" in highlights
+
+ API-Server.md:
+ - Fixed "REST and WebSocket" to "WebSocket" in intro
+
+ Home.md:
+ - Fixed "REST + WebSocket" to "WebSocket" in doc index
+
+ Module-Development.md:
+ - Fixed rdp.rs comment from "xfreerdp/rdesktop wrapper" to "Native RDP auth (X.224, TLS, CredSSP/NTLM)"
+
+ Testing-QA.md:
+ - Updated module counts 181 to 240 (183 exploits, 27 scanners, 29 creds, 1 plugin) in two places
+ - Fixed CLI example from --command scanner --module to -m scanners/sample_scanner -t
+ - Replaced all Bearer token API smoke tests with curl /health
+
+ API-Usage-Examples.md:
+ - Changed title from "REST API" to "WebSocket API"
+ - Rewrote the auth note to reflect PQ WebSocket sessions
+ - Removed 5 non-existent endpoints: /api/validate, /api/status, /api/ips, /api/auth-failures, /api/rotate-key
+ - Updated module count from 181 to 240
+
+ Changelog.md:
+ - Fixed "182 exploit modules" to "183"
+
+
+ aws-lc-sys is completely gone from the dependency tree. Here's a summary of the changes:
- The API server should now show Authorized clients: 1 with the gui-backend key, and the GUI backend should show a successful PQ connection instead of the API key error. No more plaintext — everything goes through the PQ-encrypted
- channel.
+ Files changed:
+ - Cargo.toml — Switched reqwest, rustls, and tokio-rustls from aws-lc-rs (default) to ring crypto backend
+ - src/modules/scanners/ssl_scanner.rs:106 — Changed rustls::crypto::aws_lc_rs::default_provider() to rustls::crypto::ring::default_provider()
+
+ The 317s aws-lc-sys C library build is eliminated. ring compiles from Rust+asm in ~15s. Next clean build should be significantly faster.
+
+● Build succeeded with zero warnings. All 11 warnings are resolved:
+
+ 1. Context import (telnet_iac_flood.rs) — removed unused import
+ 2. SE constant (telnet_iac_flood.rs) — used in build_sb_bomb() to add valid-looking SE-terminated SB frames that force parsers to check each boundary
+ 3. load_lines_batched / load_lines_uncapped (utils/mod.rs) — now used via ftp_bruteforce and run_bruteforce_streaming
+ 4. run_bruteforce_streaming (creds/utils.rs) — wired into ftp_bruteforce as the default bruteforce engine
+ 5. decode / nibble / DecodeError (native/hex.rs) — tightvnc now uses the vendored crate::native::hex::decode instead of the external hex crate
+ 6. load_lines_batched (utils/modules.rs) — used by run_bruteforce_streaming's batch processing path
+ 7. STREAMING_THRESHOLD (utils/modules.rs) — used by ftp_bruteforce to decide streaming vs in-memory
+ 8. load_lines_uncapped (utils/modules.rs) — used by ftp_bruteforce for loading passwords without the 100MB cap
+ 9. file_size (utils/modules.rs) — used by ftp_bruteforce and run_bruteforce_streaming for size checks
+
+
+ Migrated src/modules/creds/utils.rs → src/utils/bruteforce.rs
+
+ - Created src/utils/bruteforce.rs with the full content (bruteforce engine, mass scan engine, combo generation, subnet scanning, IP utilities, stats tracking)
+ - Registered as pub mod bruteforce in src/utils/mod.rs with re-exports for all 24 public items
+ - Bulk-updated all ~203 consumer files from crate::modules::creds::utils:: to crate::utils::
+ - Removed old src/modules/creds/utils.rs and its pub mod declaration from creds/mod.rs
+ - Removed 2 unused re-exports (BruteforceStats, format_duration) that are only used internally within bruteforce.rs
+
+
+ Fixed. Now sub-category grouping uses the second-level directory (exploits/voip, exploits/webapps, exploits/windows), so all webapps modules stay in one contiguous block regardless of whether they're flat or nested in subdirs.
+
+ Root cause: 50 concurrent tokio::spawn tasks each calling print_banner() with 6 separate mprintln!() calls — lines from different tasks interleave on stdout.
+
+ Fixes applied:
+
+ 1. src/output.rs — Added STDOUT_MUTEX, _mprint_block() function, and mprintln_block! macro for atomic multi-line output that holds a lock across all lines.
+ 2. src/modules/exploits/honeytrap/docker_panic.rs — Banner is now skipped in batch mode (is_batch_mode()) and uses mprintln_block! for atomic output when it does print.
+ 3. src/modules/exploits/honeytrap/ftp_panic.rs — Same fix applied.
+
+ The mprintln_block! macro is now available for any module that needs garble-free multi-line output under concurrency. The batch mode guard is the primary fix — banners are redundant noise when scanning 10,000 hosts.
+
+
+ MEDIUM (5)
+
+ ┌──────────────────────────┬───────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ File │ Line │ Bug │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 913-915 │ Lockout pause is per-task, not global. When 10+ consecutive errors trigger the 30s pause, only ONE task sleeps — other concurrent tasks keep hammering the target, defeating rate-limit │
+ │ │ │ recovery. Needs a shared AtomicBool pause flag checked by all tasks. │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 1203-1211 │ Subnet bruteforce silently drops non-keyword errors. If LoginResult::Error message doesn't contain "refused"/"timeout"/"reset", it falls through with no logging, no error count, no stats. │
+ │ │ │ Makes debugging impossible. │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 495-524 │ Random scan mode has no task join. After the loop breaks at max_checks, spawned tasks are orphaned — in-flight results may be lost before the file writer flushes. │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ redis_bruteforce.rs │ 648-651 │ Comment/code mismatch on -NOAUTH. Comment says "treat as success for empty password" but code returns Ok(false) (failure). If the intent matches the comment, the return should be Ok(true). │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ http_basic_bruteforce.rs │ 506-507 │ Redirect without Location header treated as auth success. A 301/302 with no Location is a broken response, not evidence of successful auth. Should return Err or Ok(false). │
+ └──────────────────────────┴───────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+ LOW (3)
+
+ ┌─────────────────────┬─────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ File │ Line │ Bug │
+ ├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ l2tp_bruteforce.rs │ 812 │ Dead code. if pkt.payload.len() < 3 { continue; } is unreachable because the outer condition already requires len() > 4. │
+ ├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ ftp_bruteforce.rs │ 333 │ IPv6 hostname extraction broken for TLS. Splitting ::1 on ':' gives empty first element. Mitigated by danger_accept_invalid_hostnames(true) so no functional impact. │
+ ├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 901/909 │ Backoff off-by-one. First retry uses attempt=1 giving 2x base delay. Doc says "0-indexed". Minor — just starts slightly slower than expected. │
+ └─────────────────────┴─────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+ 1. Global lockout pause (bruteforce.rs) — Added shared paused AtomicBool. When lockout is detected, one task sets the flag via swap (preventing duplicate pauses), sleeps 30s, then clears it. All other tasks spin-wait on the flag
+ before attempting login.
+
+ 2. Backoff off-by-one (bruteforce.rs) — Changed backoff_delay(500, retries as u32, 8) to backoff_delay(500, (retries - 1) as u32, 8) so first retry uses 1x base (500ms) instead of 2x.
+
+ 3. Random scan task drain (bruteforce.rs) — Added semaphore drain loop after the random scan loop exits, ensuring all in-flight tasks complete before the function returns.
+
+ 4. Subnet error logging (bruteforce.rs) — Non-keyword errors in subnet bruteforce now log the error in verbose mode instead of silently dropping.
+
+ 5. Redis -NOAUTH (redis_bruteforce.rs) — Fixed return from Ok(false) to Ok(true) to match intent: empty-password probe against a no-auth server should report success.
+
+ 6. HTTP redirect without Location (http_basic_bruteforce.rs) — Changed from Ok(true) (false positive) to Err(...) so the bruteforce engine treats it as a retryable protocol error.
+
+ 7. L2TP dead code (l2tp_bruteforce.rs) — Relaxed outer condition from len() > 4 to !payload.is_empty(), making the inner len() < 3 guard reachable for small PPP data packets (3-4 bytes without HDLC header).
+
+ 8. FTP IPv6 TLS hostname (ftp_bruteforce.rs) — Replaced single-pass split(&[']', ':']) with bracket-aware parser: bracketed targets split on ], bare targets split on :.
+
+
+ Summary: All 71 modules with print_banner() are now fixed.
+
+ Changes across all files:
+ 1. src/output.rs — Added STDOUT_MUTEX, _mprint_block() function, and mprintln_block! macro for atomic multi-line output
+ 2. 71 module files — Every print_banner() call is now guarded by if !crate::utils::is_batch_mode(), and every print_banner() body uses mprintln_block! for atomic output
+
+
+ Fixed. The telnet_bruteforce header is now guarded by is_batch_mode() and uses mprintln_block! for atomic output.
+
+ ● Fixed the batch-mode output spam. When the framework's mass-scan dispatcher (src/commands/mod.rs:207) spawns 50 concurrent tasks to run a module against random/CIDR/file targets, each task was hitting the single-target path, printing
+ its banner, per-host target info, and summary — producing 50× interleaved output.
+
+ Changes:
+ - proxy_scanner.rs / reflect_scanner.rs: gate the banner and all single-target verbose/summary mprintln! lines on !is_batch_mode(). Hits still print; decoration doesn't.
+ - All 161 other banner functions under src/modules/ (display_banner, print_banner, banner): added if crate::utils::is_batch_mode() { return; } as the first line.
+ - The existing cached_prompt mutex already ensures prompts fire once per batch; banner suppression was the missing piece.
+
+
+ Prompt cache — verified OK. All 8 cfg_prompt_* wrappers in src/utils/prompt.rs route through cached_prompt(). The BATCH_CACHE (mutex-guarded HashMap) serializes the first prompt and hands subsequent concurrent tasks the
+ cached answer. Generation counter in src/context.rs clears the cache between successive scans.
+
+ Batch mode scope — verified OK. enter_batch_mode() is entered before every fan-out at src/commands/mod.rs:208 (random), :318 (file), :413 (CIDR), and the BatchGuard drops only after the semaphore barrier awaits all tasks. RUN_CONTEXT
+ is explicitly re-scoped inside each spawned task so the per-task prompt-cache pointer survives tokio::spawn.
+
+ Banner/decoration output — fixed this turn. Added if crate::utils::is_batch_mode() { return; } to 161 display_banner/print_banner/banner fns across src/modules/, plus gated per-host verbose blocks in proxy_scanner.rs and
+ reflect_scanner.rs.
+
+ Interactive stdin bypasses — fixed. 7 modules (esxi_vsock_client, avtech CVE-2024-7029, react2shell, sshpwn_session × 2, tp_link_vn020_dos, jenkins_2_441_lfi) had post-exploit interactive shells gated only on api_mode. In batch mode
+ these would hang the concurrent task. Now gated on api_mode || is_batch_mode() with a clear error message.
+
+ Content — wordlists cached (via cfg_prompt_wordlist + prompt cache); state-file dedup prevents duplicate-IP rework; HTTP clients rebuilt per-task by design. Honeypot-check caching flagged as redundant (CIDR/random scans produce unique
+ IPs; existing state file already dedupes).
+
+ Summary of what's wired for mass-scan mode:
+
+ 1. Framework dispatcher (src/commands/mod.rs) handles random/CIDR/file targets for every module by spawning concurrent tasks that call module::run(single_ip). Every module now supports mass-scan mode transparently.
+ 2. Per-task module timeout added to all three mass-scan paths (random, file, CIDR). Reads setg module_timeout (default 60s). If any module hangs on a single host, tokio::time::timeout kills the task and frees the concurrency slot —
+ prevents one slow/bad module from starving the scan.
+ 3. Prompt cache — 8 cfg_prompt_* wrappers route through cached_prompt() + BATCH_CACHE. First concurrent task prompts; the rest pull the cached answer. Generation counter clears cache between successive scans.
+ 4. Banner suppression — 163 display_banner/print_banner/banner fns in src/modules/ early-return when is_batch_mode() is true. Verified 0 remaining unguarded.
+ 5. Interactive shell guards — 7 post-exploit interactive sessions (SSH, camera, esxi, react, jenkins, tplink) return errors instead of blocking stdin in batch mode.
+ 6. Per-host verbose output — proxy_scanner and reflect_scanner suppress target info, probe progress, and summary tables in batch mode (hits still print).
+ 7. State-file dedup — is_ip_checked/mark_ip_checked use POSIX atomic small-write appends; double-probe on race is benign.
+ 8. RUN_CONTEXT re-scoped inside every tokio::spawn so task-locals and prompt-cache pointers survive the spawn.
+
+ Tunable knobs: setg concurrency , setg max_random_hosts , setg module_timeout , setg honeypot_detection n.
+
+ test_mass_scan.rc)
+ ⎿ Wrote 8 lines to test_mass_scan.rc
+ 1 setg concurrency 2
+ 2 setg module_timeout 5
+ 3 setg honeypot_detection n
+ 4 setg ports 8080
+ 5 use scanners/proxy_scanner
+ 6 t 169.254.0.0/30
+ 7 run
+ 8 exit
+
+● Bash(timeout 60 ./target/debug/rustsploit -r test_mass_scan.rc 2>&1 | tee /tmp/rsf_test.log; echo "---EXIT: $?")
+ ⎿ Welcome to RustSploit Shell (inspired by RouterSploit)
+ Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.
+ … +21 lines (ctrl+o to expand)
+ ⎿ (timeout 1m 30s)
+ ⎿ Allowed by auto mode classifier
+
+● Test passed. Key observations from the output:
+
+ ┌─────────────────┬───────────────────────────┬───────────────────────────────────────────────────────────────┐
+ │ Check │ Before fix │ Result now │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Banner display │ 5+ interleaved copies │ Zero (suppressed in batch mode) │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Prompt count │ Once per task (4×) │ Once total — Timeout (ms) [5000]: appeared exactly once, │
+ │ │ │ cached for all 4 tasks │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Framework │ n/a │ [*] Subnet: 169.254.0.0/30 (4 hosts) — concurrency 2 — CIDR │
+ │ dispatch │ │ path engaged │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Progress │ n/a │ [*] Progress: 1/4 hosts (25.0%) | 0 ok | 0 err │
+ │ reporting │ │ │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Per-task │ hung forever on │ module_timeout=5s → 3 failures reported, scan finished in │
+ │ timeout │ unreachable IPs │ ~20s total │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Summary │ 50× per-task summaries │ One === Subnet Scan Summary === block │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Exit │ — │ Clean EXIT: 0 │
+ └─────────────────┴───────────────────────────┴───────────────────────────────────────────────────────────────┘
+
+ Confirmed: banner guard, prompt cache, batch dispatcher, and per-task timeout all wire correctly end-to-end.
+
+ Removed run_all:
+ - Deleted the run_all command handler, tab completion entry, help text, man page, and all aliases (ra, runall)
+ - ra now maps to run (kept as alias for muscle memory)
+ - WS API "run_all" routes to rpc_run_module for backward compat
+ - Cleaned up dead code: IPV6_PREFIX_THRESHOLD constant, get_target_subnet() method
+
+ Audit fixes (injection/edge cases):
+ - export: replaced weak path.contains("..") check with proper validate_export_path()
+ - spool: added validate_safe_file_path() before opening file
+ - resource: already fixed — validate_safe_file_path() before reading
+ - makerc: already fixed — validate_safe_file_path() before writing
+ - setg: already fixed — sanitize_string_input() on key/value
+ - hosts add: validates input through sanitize_target() before storing
+ - services add: validates host via sanitize_target(), validates port is 1-65535 (was unwrap_or(0))
+ - workspace: added 64-char length limit on names
+
+ Tab completion fix (src/shell.rs): When input exactly matches a command (e.g. "run"), return only that exact match
+ — prevents rustyline from auto-completing "run" to "run_all" when pressing space/tab.
+
+ Injection prevention (kept from before):
+ - resource command: validates path with validate_safe_file_path() before reading
+ - makerc command: validates path with validate_safe_file_path() before writing
+ - setg command: sanitizes key and value with sanitize_string_input() before storing
+
+context.rs:
+
+ 1. BatchGuard race — replaced AtomicBool with AtomicUsize refcount. Nested/concurrent batches now work correctly —
+ batch stays active until all guards drop. Generation only bumps on the 0→1 transition.
+ 2. TOCTOU gap — fixed by using fetch_add with AcqRel ordering and only incrementing BATCH_GEN when the refcount
+ goes from 0 to 1 (single atomic check).
+ 3. Prompt cache size limit — added MAX_PROMPT_CACHE_ENTRIES = 256 and cache_insert() helper that rejects inserts
+ when full. Both the global batch cache and task-local cache paths in prompt.rs now use it.
+ 4. Visibility — set_cache_generation and cache_generation narrowed to pub(crate).
+
+
+ Plan: Audit fixes for api.rs — SSRF, validation gaps, fail-open
+
+ Context
+
+ Security audit of src/api.rs found critical gaps in SSRF protection, missing IP range blocks, validation
+ inconsistencies across the WS/MCP layers, and a fail-open DNS policy. These fixes harden the API server against
+ internal network access and bypass techniques.
+
+ ---
+ Fix 1: Complete is_blocked_ip — add loopback, private, and IPv6 ranges
+
+ File: src/api.rs:102-123
+
+ Current code only blocks: 169.254.x.x (link-local), 168.63.129.16 (Azure), 100.100.100.200 (Alibaba),
+ fd00:0ec2::/32.
+
+ Add blocking for:
+ - 127.0.0.0/8 (loopback)
+ - 10.0.0.0/8 (private)
+ - 172.16.0.0/12 (private)
+ - 192.168.0.0/16 (private)
+ - 0.0.0.0
+ - IPv6 ::1 (loopback)
+ - IPv6 fe80::/10 (link-local)
+ - IPv6 fc00::/7 (ULA)
+
+ fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
+ match ip {
+ std::net::IpAddr::V6(v6) => {
+ if v6.is_loopback() { return true; } // ::1
+ let segs = v6.segments();
+ if segs[0] == 0xfd00 && segs[1] == 0x0ec2 { return true; }
+ if segs[0] & 0xfe00 == 0xfc00 { return true; } // fc00::/7
+ if segs[0] & 0xffc0 == 0xfe80 { return true; } // fe80::/10
+ if let Some(v4) = v6.to_ipv4_mapped() {
+ return is_blocked_ipv4(v4);
+ }
+ false
+ }
+ std::net::IpAddr::V4(v4) => is_blocked_ipv4(v4),
+ }
+ }
+
+ fn is_blocked_ipv4(v4: std::net::Ipv4Addr) -> bool {
+ let o = v4.octets();
+ o == [0,0,0,0] // 0.0.0.0
+ || o[0] == 127 // 127.0.0.0/8
+ || o[0] == 10 // 10.0.0.0/8
+ || (o[0] == 172 && (o[1] & 0xf0) == 16)// 172.16.0.0/12
+ || (o[0] == 192 && o[1] == 168) // 192.168.0.0/16
+ || (o[0] == 169 && o[1] == 254) // 169.254.0.0/16
+ || o == [168, 63, 129, 16] // Azure metadata
+ || o == [100, 100, 100, 200] // Alibaba metadata
+ }
+
+ Fix 2: Harden is_blocked_target URL parsing
+
+ File: src/api.rs:26-100
+
+ Current gaps:
+ - @ authority confusion (http://evil@169.254.169.254/)
+ - Only strips http:// and https://, other schemes pass through
+ - No URL-decoding
+
+ Fix the host extraction:
+ 1. Use url::Url::parse for proper URL parsing (already a dependency)
+ 2. Extract host_str() which handles @, port, and schemes correctly
+ 3. Fall back to the current manual parsing for non-URL targets (bare IPs/hostnames)
+ 4. Percent-decode the host before checking against blocklists
+
+ Fix 3: Fail-closed on DNS error in is_blocked_target_resolved
+
+ File: src/api.rs:152
+
+ Change Ok(Err(_)) | Err(_) => false to true:
+ Ok(Err(_)) | Err(_) => true, // fail-closed: block if DNS fails
+
+ Fix 4: Add is_blocked_target_resolved to rpc_set_target
+
+ File: src/ws.rs:450-466
+
+ Currently only calls is_blocked_target() (static check). Add the async DNS resolution check:
+ if crate::api::is_blocked_target_resolved(target).await {
+ return Err(rpc_err("SSRF_BLOCKED", "Target resolves to blocked address"));
+ }
+
+ Fix 5: Add missing validation in MCP tools
+
+ File: src/mcp/tools.rs
+
+ - handle_set_target (~line 478): Add validate_target() + is_blocked_target_resolved()
+ - handle_module_info (~line 456): Add validate_module_name()
+ - handle_run_module (~line 504): Add validate_target() + validate_module_name()
+
+ ---
+ Files to Modify
+
+ ┌────────────────────┬─────────────────────────────────────────────────────────┐
+ │ File │ What │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/api.rs:102-123 │ Rewrite is_blocked_ip with complete range coverage │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/api.rs:26-100 │ Use url::Url for robust host extraction, percent-decode │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/api.rs:152 │ Fail-closed on DNS error │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/ws.rs:450-466 │ Add is_blocked_target_resolved to rpc_set_target │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/mcp/tools.rs │ Add missing validation to 3 handlers │
+ └────────────────────┴─────────────────────────────────────────────────────────┘
+
+ ---
+ Verification
+
+ 1. cargo check — no compilation errors
+ 2. Unit-test is_blocked_ip with: 127.0.0.1, 10.0.0.1, 172.16.0.1, 192.168.1.1, 0.0.0.0, ::1, fe80::1 — all should
+ return true
+ 3. Test is_blocked_target with: http://evil@169.254.169.254/, ftp://metadata.google.internal/,
+ HTTP://METADATA.GOOGLE.INTERNAL/ — all should return true
+ 4. Test DNS fail-closed: target with unresolvable hostname should be blocked
+╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+
+ Claude has written up a plan and is ready to execute. Would you like to proceed?
+
+ ❯ 1. Yes, auto-accept edits
+
+
+ lean build, zero warnings. Here's the summary:
+
+ Fix 1 — is_blocked_ip rewritten with complete coverage:
+ - Added: loopback (127.x), private (10.x, 172.16.x, 192.168.x), 0.0.0.0, IPv6 ::1, fe80::/10, fc00::/7
+ - Split into is_blocked_ip (v4/v6 dispatch) + is_blocked_ipv4 for clarity
+
+ Fix 2 — is_blocked_target hardened against bypass:
+ - Uses url::Url::parse first for proper host extraction (handles @, ports, all schemes)
+ - Percent-decodes hostnames before checking blocklists
+ - Strips userinfo (@) in fallback manual parsing
+ - Blocks file:// scheme entirely
+ - Strips query strings and fragments from host extraction
+
+ Fix 3 — Fail-closed on DNS error:
+ - is_blocked_target_resolved now returns true when DNS fails/times out
+
+ Fix 4 — rpc_set_target (ws.rs):
+ - Added is_blocked_target_resolved() async DNS check
+
+ Fix 5 — MCP tools validation:
+ - handle_module_info: added validate_module_name()
+ - handle_set_target: made async, added validate_target() + is_blocked_target() + is_blocked_target_resolved()
+ - handle_run_module: added validate_module_name() + validate_target() + is_blocked_target()
+ - handle_check_module: added validate_module_name() + validate_target() + is_blocked_target()
+
+
+ output.rs Audit
+
+ 1. Silent output drop on poisoned mutex (lines 34, 42)
+ if let Ok(mut guard) = self.stdout.lock() {
+ If the mutex poisons (a thread panicked while holding it), push_stdout/push_stderr silently discard output. No
+ log, no indication.
+
+ 2. Silent drop when buffer full (lines 35-36, 43-44)
+ When guard.len() >= MAX_BUFFER_LINES, new output is silently lost. No truncation warning emitted.
+
+ 3. No size limit on OutputAccumulator::add_finding (line 282-285)
+ Unlike OutputBuffer (capped at 100K lines), findings have no cap. A module generating endless findings grows
+ memory unboundedly.
+
+ 4. add_finding silently swallows poisoned mutex (line 283)
+ Same if let Ok(mut guard) pattern — findings dropped silently on poison.
+
+ 5. drain_stdout/drain_stderr can OOM (lines 50-54)
+ guard.join("") on 100K lines creates one massive allocation. No streaming drain.
+
+ 6. _mprint_block stdout/spool divergence (lines 184-193)
+ Writes to stdout first, then spool. If spool fails mid-way (break on line 190), remaining lines are in stdout but
+ not spool. The two outputs diverge.
+
+ 7. _mprint_raw doesn't spool (lines 162-171)
+ _mprint_line writes to spool, but _mprint_raw (used by mprint!) does not. Any module using mprint! instead of
+ mprintln! bypasses spool logging.
+
+ ---
+ Modules Error Swallowing — Top Findings
+
+ Critical — data loss:
+ - loot.rs:246 — atomic rename of loot index silently ignored (let _ = rename)
+ - loot.rs:49 — read error on loot_index.json returns empty vec (all loot lost)
+ - cred_store.rs:68 — read error on creds.json returns empty vec (all creds lost)
+
+ High — network/scan:
+ - port_scanner.rs:474 — let _ = stream.write_all(&payload) — probe never sent, false negatives
+ - ssh_scanner.rs:142-143 — socket timeout setup failures ignored, scanner can hang
+ - commands/mod.rs:575 — subnet mask overflow returns 0 via unwrap_or(0)
+
+ High — jobs/MCP:
+ - jobs.rs:79 — unwrap_or_default() on poisoned RwLock returns empty output
+ - mcp/client.rs:128 — MCP server shutdown timeout ignored, process leak
+
+
+
+ 1. cred_store.rs — backup copy and file read errors now logged
+ 2. jobs.rs — poisoned RwLock recovered instead of returning empty data
+ 3. workspace.rs — backup, read, serialize, and write errors all logged; save() restructured with proper error
+ branches
+ 4. port_scanner.rs — probe write failure explicitly handled (proceeds to banner grab)
+ 5. ssh_scanner.rs — socket timeout setup failure skips to next address
+ 6. mcp/client.rs — shutdown timeout and kill errors logged; early return on clean exit
+ 7. commands/mod.rs — simplified bit shift (u32 direct, safe due to prefix>=32 guard)
+
+
+ Finding 1 — Error swallowing: backup copy on line 30
+ src/global_options.rs:30 — let _ = std::fs::copy(&file_path, &backup); silently drops backup failure when
+ corrupted JSON is detected.
+
+ Finding 2 — Error swallowing: file read error on line 34
+ src/global_options.rs:34 — Err(_) => HashMap::new() silently loses all global options on a read error without
+ telling the user.
+
+ Finding 3 — set() always returns true (dead code in WS layer)
+ src/global_options.rs:47-55 — set() unconditionally returns true. The WS handler at ws.rs:615 checks if
+ !...set(...).await which can never be false. That error path is dead code.
+
+ Finding 4 — No size limit on the options map
+ The shell setg sanitizes via sanitize_string_input and the WS rpc_set_option caps key to 256 chars and value to
+ 4096 chars, but GlobalOptions::set() itself has no limits. The MCP handle_set_option at mcp/tools.rs:792-796 calls
+ set() with zero validation — no key length check, no value length check, no sanitization. An MCP client can stuff
+ arbitrary-length keys/values and create unlimited entries.
+
+ Finding 5 — try_get silently returns None on write-contention
+ src/global_options.rs:78-80 — When another task holds the write lock, try_get returns None as if the option
+ doesn't exist. Callers like commands/mod.rs:214-225 fall back to defaults (concurrency=50, timeout=60s) when the
+ lock is briefly held during a save — user's configured values are silently ignored during those moments.
+
+ Finding 6 — File target path ignores concurrency global option
+ src/commands/mod.rs:343 — hardcodes let concurrency = 50usize; instead of reading from global options like the
+ random-scan and CIDR paths do.
+
+
+ 1. Error swallowing (line 30) — backup copy failure now logged
+ 2. Error swallowing (line 34) — file read error now logged
+ 3. set() always true — now validates key length (1-256), value length (max 4096), and entry cap (1024); returns
+ false on rejection, making the WS error path live
+ 4. MCP no validation — handle_set_option now checks set() return and reports errors
+ 5. try_get write-contention — now retries up to 50ms instead of silently returning None on a briefly-held write
+ lock
+ 6. File target hardcoded concurrency — now reads concurrency from global options like the other scan paths
+
+
+All 4 bugs fixed:
+
+ 1. shell.rs:674 — creds add now reports invalid port input with a clear error instead of silently storing port 0
+ 2. ws.rs:274, 285 — subscribe/unsubscribe now reject out-of-range job IDs with an INVALID_JOB_ID error
+ 3. config.rs:74-82 — canonical path length now re-checked against MAX_TARGET_LENGTH after resolution
+ 4. pq_middleware.rs:151 — response body read wrapped in 30s timeout, returns GATEWAY_TIMEOUT on stuck handlers
+
+
+ - Bug A: ws.rs:539-568 — rpc_run_module stores prompts as dead prompt_{k} keys in GLOBAL_OPTIONS instead of using
+ ModuleConfig.custom_prompts like MCP does. Also pollutes GLOBAL_OPTIONS persistently with per-run settings
+ (concurrency, wordlists, etc.) that leak across runs.
+ - Bug B: mcp/tools.rs:852 — handle_switch_workspace missing name validation that WS enforces.
+ - Bug C: mcp/tools.rs:761 — handle_add_loot missing length validation that WS enforces.
+
+ Fix A: WS rpc_run_module — use per-run custom_prompts
+
+
+
+
+● Fix B: MCP handle_switch_workspace validation
+
+● Fix C: MCP handle_add_loot length validation
+
+ A. ws.rs rpc_run_module — refactored to build a per-run ModuleConfig.custom_prompts map (matching MCP's pattern)
+ and call run_with_context_target. This fixes three bugs at once:
+ - Dead prompt_{k} keys are gone — prompts now flow through custom_prompts which the cfg_prompt_* chain actually
+ reads
+ - No more persistent GLOBAL_OPTIONS pollution — concurrency/port/etc. are scoped to the request instead of leaking
+ across every subsequent run
+ - target prompt injection guarded (stripped like MCP already does)
+ - Response now includes captured stdout/stderr from the per-task buffer
+
+ B. MCP handle_switch_workspace — now rejects invalid names with same 1-64 alphanumeric+dash+underscore rule as WS.
+
+ C. MCP handle_add_loot — now enforces same length caps as WS (host/loot_type ≤256, description ≤4096, data
+ ≤100MB).
+
+
+ A. ws.rs rpc_run_module — 4 gaps
+
+ A1. combo_mode not validated for shell metacharacters (ws.rs:568-570)
+ The validation block at lines 515-526 checks username_wordlist, password_wordlist, output_file, and prompts.* for
+ shell metacharacters — but combo_mode is a string param that's folded straight into custom_prompts with no check.
+ A client can inject ; rm -rf / via combo_mode.
+
+ A2. Background job path silently drops all per-run config (ws.rs:531-537)
+ if background {
+ match crate::jobs::JOB_MANAGER.spawn(module.to_string(), target.to_string(), verbose) { ... }
+ }
+ When background=true, the entire prompts/port/wordlists/concurrency/stop_on_success/output_file/combo_mode object
+ is ignored. The user sends a fully-configured background run and gets a stripped-down module execution with
+ defaults. This is a major wiring gap — only foreground runs get the new custom_prompts treatment.
+
+ A3. No module existence check (ws.rs:494)
+ MCP checks discover_modules().contains(&module_path) at mcp/tools.rs:543 before spawning. WS doesn't — a typo'd
+ module name goes through validation, enters run_module, and fails deep inside dispatch with a worse error.
+
+ A4. Port not range-validated (ws.rs:550)
+ if let Some(port) = params.get("port").and_then(|v| v.as_u64()) {
+ custom_prompts.entry("port".into()).or_insert_with(|| port.to_string());
+ }
+ A port of 99999999 is accepted as a u64 and stringified. cfg_prompt_port will reject it downstream, but the early
+ rejection MCP has (via u16_param) is cleaner. Minor.
+
+ B. MCP handle_switch_workspace — 1 minor gap
+
+ B1. is_alphanumeric() accepts Unicode letters
+ char::is_alphanumeric() returns true for CJK characters, Cyrillic, etc. A workspace named 工作 or админ passes
+ validation and becomes a filename. Not exploitable, but inconsistent with the "alphanumeric+dash+underscore" spec
+ a user would expect. WS has the exact same behavior — would need to tighten both if we care (change to
+ is_ascii_alphanumeric()).
+
+ C. MCP handle_add_loot — 1 minor gap
+
+ C1. Empty host rejected with misleading error
+ MCP only checks host.len() > 256, not host.is_empty(). Empty host passes MCP validation, then loot.rs::add()
+ rejects it at line 87 and returns None, surfacing as the generic "Failed to store loot (validation or I/O error)".
+ WS has the same gap but returns the same vague error. Low-impact — error is just unhelpful.
+
+ ---
+ Priority: A1 and A2 are real wiring bugs (silent data loss / injection). A3 is a UX regression. B1, A4, C1 are
+ minor.
+
+ Want me to fix A1, A2, A3?
+
+
+ A1 — combo_mode shell metacharacter check: Added to the validation block alongside wordlists and output_file
+ (ws.rs:522-525).
+
+ A2 — Background jobs now receive per-run config:
+ - jobs::spawn now takes Option; when Some, the spawned future runs inside run_with_context_target
+ with that config.
+ - rpc_run_module builds custom_prompts before branching on background, then passes the ModuleConfig to both paths.
+ - Shell's spawn call updated to pass None (preserves existing shell semantics).
+
+ A3 — Module existence check: Added discover_modules().contains(&module) at ws.rs:502-504, matching MCP's pattern.
+ Unknown modules now fail fast with a clear MODULE_NOT_FOUND error instead of deep in dispatch.
+
+Fixed (5 real bugs):
+ 1. shell.rs:149 — silent rustsploit dir creation now logs failure
+ 2. workspace.rs:54 — silent workspaces dir creation now logs
+ 3. workspace.rs:170 — silent read_dir failure in list_workspaces now logs (still returns ["default"] fallback so
+ API doesn't break)
+ 4. config.rs:394 — silent results dir creation now logs
+ 5. bruteforce.rs:998 — .unwrap() on Option replaced with explicit fallback to non-streaming path; panic no longer
+ possible if the streaming-threshold logic ever changes
+
+
+ - 3 sites in commands/mod.rs (Mass Scan / File Target Scan / Subnet Scan) — for _ in 0..concurrency { let _ =
+ sem.acquire().await } → if let Err(e) = sem.acquire_many(concurrency as u32).await { meprintln!("[!] Drain barrier
+ failed...: {}", e) }
+ - 3 sites in utils/bruteforce.rs (file/cidr/random scan loops) — same refactor
+ - 1 site in utils/bruteforce.rs:1237 (run_bruteforce) — kept ? propagation but bound the permit with let _drain to
+ silence the unused_must_use warning
+
+ Benefits: single syscall instead of N loop iterations, clearer intent (labeled "drain barrier"), and a diagnostic
+ message if the semaphore ever gets closed unexpectedly (which would indicate a real bug elsewhere).
+
+ Plan:
+ 1. Remove crossterm + ratatui imports
+ 2. Remove App struct + impl (uses TableState, Color)
+ 3. Remove draw_ui function (~260 lines of ratatui drawing)
+ 4. Replace the TUI event loop in run() (~315 lines) with a rustyline REPL
+ 5. Update Color references in helpers (rssi_color, DeviceStatus::color,
+ execute_exploit's log channel) — map ratatui colors to colored::Color
+ (DarkGray→BrightBlack, LightRed→BrightRed, etc.)
+ 6. REPL commands: scan, list, select , test, exploit, fmdn, write
+ [count], audio connect, listen, record, talk, stop, quit/exit, help
+ 7. State (selected device, audio process, device list) kept in a plain
+ struct; rustyline handles input, helpers log directly via mprintln!
+
+ Side effects:
+ - Lose the live-updating device table (ratatui's strength)
+ - Gain: no raw-mode side-effects, plays nicely in embedded/API usage,
+ users can scroll history
+ - ratatui + crossterm deps may be removable from Cargo.toml if no other
+ module uses them — want me to check?
+
+
+ wpair.rs audit — verified findings
+
+ Critical
+
+ C1. aes_encrypt / aes_decrypt will panic on short buffers — wpair.rs:965-978
+ &key[0..16] and &data[0..16] index-slice without bounds checks. All current
+ callers pass 16-byte buffers so it's safe today, but aes_decrypt is called on
+ peripheral-returned data at 1112 after only a len() >= 16 caller-side check
+ — any future refactor that drops that check turns untrusted BLE data into an
+ in-process panic.
+
+ High
+
+ H1. Stale selected index after re-scan — wpair.rs:2271-2282 (cmd_scan)
+ After scan, if a device was previously selected, the code only resets
+ selected when count == 0 or when selected.is_none(). If the user had select 3
+ and re-scans, selected stays at 3 — now pointing at a different peripheral
+ in the new results. Silent re-binding.
+
+ H2. Passkey-confirmation write failure silently ignored — wpair.rs:1593-1595
+ let _ = peripheral.write(passkey_char, &encrypted_pk,
+ WriteType::WithoutResponse).await;
+ The exploit then reports success and attempts bonding — but if this write
+ failed, the confirmation never reached the device. User sees a false-positive
+ "EXPLOIT COMPLETE" in the log.
+
+ H3. flood_account_keys has no pacing — wpair.rs:1816-1843
+ Tight for i in 0..count { peripheral.write(...).await } loop with no sleep.
+ Real Fast Pair devices rate-limit GATT writes; later writes in the flood are
+ likely to fail and we stop at the first error (break on Err). Adding a short
+ configurable delay (e.g. 50-100ms) would materially increase success rate.
+
+ H4. Four of six KBP strategies use a half-zero shared secret — wpair.rs:1033,
+ 1041, 1070, 1080
+ Legacy, RawKbp, ExtendedResponse, ActionRequest copy a [u8; 8] salt into
+ shared_secret[0..8] and leave [8..16] as zero — so the AES-128 key used for
+ the subsequent account-key encryption has only 64 bits of entropy.
+ RawWithSeeker and Retroactive correctly fill all 16 bytes. Verify against the
+ Fast Pair spec; if the expected salt for these strategies really is 8 bytes,
+ this is correct — but the current code has no unit-test or doc link
+ documenting that.
+
+ Medium
+
+ M1. peripheral.disconnect() return discarded — seven sites (1239, 1258, 1670,
+ 1722, 1752, 1852 in wpair's BT helpers)
+ Per your "no silent let _" directive, these should at least be logged (even
+ at tracing::debug). Disconnect failures here indicate BLE state inconsistency
+ that could affect the next operation.
+
+ M2. No bonding cleanup after exploit — wpair.rs:1512-1545, 1615-1657
+ bluetoothctl pair creates a persistent bond on the local Kali system.
+ There's no corresponding remove after the exploit run, so each target
+ leaves a stale bonded entry that accumulates and may later auto-reconnect.
+
+ M3. Discovered bond addresses / keys not stored in cred_store —
+ wpair.rs:1421-1435, 1537-1540
+ BONDING SUCCESSFUL! and BR/EDR address obtained are logged only to the REPL.
+ Unlike every other cred-discovering module (SSH, SMTP, MQTT, etc.), wpair
+ doesn't call store_credential/store_loot, so findings vanish at exit.
+
+ Low / missing features
+
+ L1. No direct-target mode — run(_target: &str) ignores the target argument;
+ you must scan then select. The standard pattern in other modules is to accept
+ a MAC directly. Add: set target AA:BB:CC:DD:EE:FF → skip scan, build a
+ FastPairDevice from address + light discovery.
+
+ L2. No output-file option — the API-mode handler (run_api_mode) prints to
+ stdout; there's no output_file cfg_prompt like every other scanner.
+ Bruteforce / batch runs can't persist findings easily.
+
+ L3. No authorization / scope check — cfg_prompt_yes_no("authorized", "Confirm
+ authorized test", false) gate is missing. Bluetooth exploitation typically
+ requires explicit consent confirmation per the project's pattern in other RCE
+ modules.
+
+ L4. aes_encrypt / aes_decrypt have no unit tests — a known-answer test
+ against a Fast Pair spec example vector would catch any future regression in
+ key derivation.
+
+ L5. REPL has no tab-completion — shell.rs has rich completion via
+ RsfCompleter; wpair's rustyline editor is created with default config. Easy
+ win: add a WpairCompleter for the 16 command keywords and device indices.
+
+ L6. render_device_table is one-shot — it only redraws on list. A watch or
+ auto-refresh option (re-render every N seconds while idle) would be closer to
+ the spirit of "live updating" you asked for. The spinner covers active ops
+ but not passive monitoring.
+
+
+● All Err(_) patterns are timeout-elapsed errors where the message already
+ describes what happened ("Connection timed out after {N}ms"). The tokio
+ Elapsed error's display is just "deadline has elapsed" — adding it would be
+ less informative than what's already there.
+
+ Final summary:
+
+ - let _ =: 0 remaining (was 7 — all converted to if let Err(e) = ... with
+ tracing::debug for disconnects and log_msg for bluetoothctl)
+ - if let Ok(...): 2 remaining — both String::from_utf8 on raw BLE bytes;
+ failure means non-UTF8 device name, which is normal and already handled by
+ the outer read-success path
+ - Err(_) =>: 6 remaining — all timeout-to-descriptive-message conversions
+ where the error detail (Elapsed) is less useful than the contextual message
+ already produced
+ - Poisoned mutex locks: 0 remaining — all converted to unwrap_or_else(|e|
+ e.into_inner())
+ - Build: clean, zero warnings
+
+
+ I found several bugs in this module. Let me analyze them:
+
+ 1. Silent error swallowing - the error handler only checks for EPERM, ALL other send errors (ENETUNREACH, ENOBUFS,
+ EINVAL, etc.) are silently dropped
+ 2. No pre-flight test - 1000 workers spawn with no validation that sending works
+ 3. No actual sendmmsg - despite advertising it, uses individual send_to calls
+ 4. Each worker creates its own socket - 1000 sockets can exhaust file descriptors, and errors are only logged for
+ worker 0
+ 5. Default 8MB stack per thread - 1000 threads = 8GB virtual memory
+
+ Let me check one more thing before writing the fix.
+
+ Searched for 1 pattern, read 1 file (ctrl+o to expand)
+
+● Now let me write the fixed module. The key changes are: shared socket pool via raw fds, actual sendmmsg batching,
+ pre-flight test, comprehensive error reporting, and smaller thread stacks.
+
+● Write(src/modules/exploits/dos/null_syn_exhaustion.rs)
+
+
+ Fix memcached_amplification.rs
+
+●
+
+
+ - Shared socket pool via raw fds (no per-worker socket creation)
+ - Pre-flight test before spawning workers
+
+
+
+ - Graceful spawn failure handling
+
+ Files being fixed:
+
+ 1. dns_amplification.rs
+ 2. memcached_amplification.rs
+
+ 4. ssdp_amplification.rs
+
+ 6. icmp_flood.rs
+ 7. udp_flood.rs
+
+
+✻ Baked for 4m 49s · 7 local agents still running
+
+※ recap: Fixing silent error swallowing and per-worker socket bugs across all 7 raw-socket DoS modules. Waiting for
+ 7 parallel agents to finish writing the fixed files, then I'll run cargo check to verify everything compiles.
+ (disable recaps in /config)
+
+ /btw with all these fixes applied how much resources are used and max possible like the improvemtn scale Fixes ap…
+
+ ├────────────────────┼─────────────────────────────────────────┼────────┤
+ │ Sockets opened │ 1000 (hits default ulimit 1024) │ max 32 │
+ ├────────────────────┼─────────────────────────────────────────┼────────┤
+ │ FD exhaustion risk │ High — workers past ~1020 silently fail │ None │
+ └────────────────────┴─────────────────────────────────────────┴────────┘
+
+ Syscall Overhead
+
+ ┌────────────────────────────┬─────────────────────────────┬─────────────────────────────┬──────────────────┐
+ │ │ Before │ After │ Improvement │
+ ├────────────────────────────┼─────────────────────────────┼─────────────────────────────┼──────────────────┤
+ │ Syscalls per 32 packets │ 32 × sendto() │ 1 × sendmmsg() │ 32x fewer │
+ │ │ │ │ syscalls │
+ ├────────────────────────────┼─────────────────────────────┼─────────────────────────────┼──────────────────┤
+ │ Kernel lock acquisitions │ 32 (one per sendto, on one │ 1 (one sendmmsg, on one of │ 32x fewer │
+ │ per 32 packets │ of 1000 sockets) │ 32 sockets) │ │
+ ├────────────────────────────┼─────────────────────────────┼─────────────────────────────┼──────────────────┤
+ │ Context switches │ Extreme — 1000 threads │ Moderate — threads do more │ Significant │
+ │ │ competing │ work per wakeup │ reduction │
+ └────────────────────────────┴─────────────────────────────┴─────────────────────────────┴──────────────────┘
+
+ Throughput Estimate
+
+ The real bottleneck is kernel socket lock contention and syscall overhead:
+
+ - Before: 1000 threads × 1000 individual sendto calls each hitting separate socket locks = massive contention,
+ many threads starved. Effective throughput likely near zero due to silent errors from fd exhaustion + the
+ surviving threads fighting each other.
+ - After: 1000 threads across 32 sockets, each doing sendmmsg(32 packets) = ~31 threads per socket lock, each
+ submitting 32 packets atomically.
+
+
+ - Fix error handler: count all errors, log first in verbose, stop on EPERM/EACCES, backoff on ENOBUFS, break after 500 consecutive
+ - Track local_errs, consecutive_errs, error_logged
+ - Flush to global_errors
+
+ 4. Rewrite worker_thread_standard:
+ - Add global_errors: Arc param
+ - Fix socket creation to log for ALL workers, not just worker_id == 0
+ - Fix error handler: count all errors, log first in verbose mode for worker 0
+ - Track and flush local_errs
+
+ 5. Rewrite execute_attack:
+ - For spoofed mode: create shared socket pool using create_raw_socket, extract fds, pre-flight test
+ - For standard mode: just add error counter
+ - errors_total: Arc
+ - thread::Builder::new().stack_size(128 * 1024)
+ - Handle spawn failures
+ - Pass fd (spoofed) or nothing extra (standard) plus errors to workers
+ - Show errors in stats and summary
+ - drop(sockets) at end if spoofed
+
+ Keep PacketBuilder, FastRng, gen_ipv4_public, gather_config, display_banner, run, info UNCHANGED.
+
+ Write the complete fixed file.
+
+ 10:15 AM claude-opus-4-6
+● All 7 agents are running in parallel, each fixing one DoS module. They're applying the same fix pattern from the null_syn_exhaustion fix:
+
+ - Shared socket pool via raw fds (no per-worker socket creation)
+ - Pre-flight test before spawning workers
+ - Comprehensive error logging (all errors, not just EPERM)
+ - Error counter in stats display and final summary
+ - 128KB thread stacks via thread::Builder
+ - Graceful spawn failure handling
+
+ Files being fixed:
+ 1. dns_amplification.rs
+ 2. memcached_amplification.rs
+ 3. ntp_amplification.rs
+ 4. ssdp_amplification.rs
+ 5. syn_ack_flood.rs
+ 6. icmp_flood.rs
+ 7. udp_flood.rs
+
+ I'll run cargo check once all agents complete to verify everything compiles.
+
+
+
+ 8 modules fixed total:
+
+ ┌────────────────────────────┬─────────────────────────────────────────────────────────────────┐
+ │ Module │ Bugs Fixed │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ null_syn_exhaustion.rs │ Silent errors, per-worker sockets, fake sendmmsg, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ dns_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ memcached_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ ntp_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ ssdp_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ syn_ack_flood.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ icmp_flood.rs │ Silent errors, per-worker sockets (spoofed), no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ udp_flood.rs │ Silent errors, per-worker sockets (spoofed), no pre-flight │
+ └────────────────────────────┴─────────────────────────────────────────────────────────────────┘
+
+ Fixes applied consistently:
+ - Shared socket pool via AsRawFd + libc::sendto — no more fd exhaustion with many workers
+ - Pre-flight test — sends one packet before spawning workers; aborts immediately with a descriptive error if it fails
+ - Comprehensive error handling — all errno values logged (not just EPERM), with ENOBUFS backoff and consecutive-error circuit breaker
+ - Error counter in live stats display and final summary
+ - 128KB thread stacks via thread::Builder — 64x less memory than default
+ - Graceful spawn failure handling — logs failures, continues with available workers
+
+● Found 11 modules with duplicate mass scan implementations. The key duplication is EXCLUDED_RANGES (copied 10 times) and inline random-IP loops instead of using the shared run_mass_scan(). Let me clean this up systematically.
+
+ First, let me check what the duplicate EXCLUDED_RANGES look like vs the canonical one:
+
+ Searched for 2 patterns (ctrl+o to expand)
+
+● 10 duplicates. Now let me check how each module imports utilities — do they already import from crate::utils?
+
+ Searched for 1 pattern (ctrl+o to expand)
+
+● They import generate_random_public_ip but define their own EXCLUDED_RANGES and parse_exclusions. Let me check if they also duplicate parse_exclusions:
+
+ Searched for 3 patterns (ctrl+o to expand)
+
+● Good — they don't duplicate parse_exclusions, only EXCLUDED_RANGES. Let me check how they use the constant:
+
+ Read 1 file (ctrl+o to expand)
+
+● The module's EXCLUDED_RANGES is a subset of the canonical one in bruteforce.rs. Let me verify the canonical one is a superset:
+
+ Read 1 file (ctrl+o to expand)
+
+● The canonical EXCLUDED_RANGES is a strict superset (includes Cloudflare + Google ranges). Now let me verify EXCLUDED_RANGES is already re-exported from utils/mod.rs:
+
+ Searched for 1 pattern (ctrl+o to expand)
+
+● Good — already exported. Now I need to replace all 10 duplicate const EXCLUDED_RANGES definitions with use crate::utils::EXCLUDED_RANGES;. Let me do them all:
+
+● Agent(Replace duplicate EXCLUDED_RANGES in all 10 modules)
+ ⎿ Done (13 tool uses · 30.0k tokens · 1m 8s)
+ (ctrl+o to expand)
+
+● The agent planned but didn't execute. Let me do the edits myself, starting with reading each file's EXCLUDED_RANGES block and imports.
+
+ Read 4 files (ctrl+o to expand)
+
+● Good. All follow the same pattern. Let me now read the remaining 6 files' imports and EXCLUDED_RANGES blocks:
+
+ Read 6 files (ctrl+o to expand)
+
+● Now I have all 10 files. Let me do all the edits in parallel — remove the const block and add EXCLUDED_RANGES to imports:
diff --git a/docs/API-Server.md b/docs/API-Server.md
index 38028c0..23b9235 100644
--- a/docs/API-Server.md
+++ b/docs/API-Server.md
@@ -1,6 +1,6 @@
# API Server
-Rustsploit includes a built-in REST API server (`src/api.rs`) with post-quantum encrypted transport and SSH-style identity key authentication. No TLS. No API keys.
+Rustsploit includes a built-in API server (`src/api.rs`, `src/ws.rs`) with post-quantum encrypted WebSocket transport and SSH-style identity key authentication. No TLS. No API keys.
---
@@ -78,6 +78,7 @@ Authentication uses SSH-style public/private key pairs with post-quantum cryptog
|--------|------|-------------|
| `GET` | `/health` | Health check |
| `POST` | `/pq/handshake` | Establish PQ-encrypted session (mutual auth) |
+| `GET` | `/pq/ws` | Upgrade to PQ-encrypted WebSocket transport |
### Protected (26 endpoints — require active PQ session)
@@ -168,7 +169,26 @@ Authentication uses SSH-style public/private key pairs with post-quantum cryptog
> All responses include `request_id`, `timestamp`, and `duration_ms` fields for observability.
-> **Total: 27 endpoints** (1 public + 26 protected) across 9 resource categories.
+> **Total: 28 endpoints** (2 public + 26 protected) across 9 resource categories, plus WebSocket transport.
+
+### WebSocket Transport
+
+`GET /pq/ws` upgrades the connection to a PQ-encrypted WebSocket. After the initial `/pq/handshake`, clients can switch to WebSocket for persistent bidirectional communication.
+
+**Features:**
+- PQ-encrypted frames using ChaCha20-Poly1305 (same security as REST)
+- Max 100 concurrent WebSocket connections
+- 30-second heartbeat interval
+- 1 MiB max frame size
+- Sub-session key derivation from the PQ handshake session
+
+**Headers required:**
+- `X-PQ-Session-Id` — session ID from `/pq/handshake`
+- Standard WebSocket upgrade headers
+
+WebSocket messages use the same JSON request/response format as REST endpoints. The WebSocket transport is ideal for long-running operations, real-time job monitoring, and persistent client connections.
+
+---
### Shell Command Endpoint
diff --git a/docs/API-Usage-Examples.md b/docs/API-Usage-Examples.md
index cc66931..bd34f86 100644
--- a/docs/API-Usage-Examples.md
+++ b/docs/API-Usage-Examples.md
@@ -1,12 +1,10 @@
# API Usage Examples
-Practical workflows for interacting with the Rustsploit REST API.
+Practical workflows for interacting with the Rustsploit WebSocket API.
> Start the server first: `cargo run -- --api`
>
-> **Note:** Direct `curl` usage requires completing a PQ handshake first (programmatic clients only). For interactive use, connect via ArcticAlopex GUI which handles the PQ session automatically. The `curl` examples below show the plaintext request/response format — in practice, all traffic is PQ-encrypted.
->
-> The `Authorization: Bearer` headers shown below are **no longer used** — authentication is via PQ identity keys established during the handshake. The examples retain the header for reference only.
+> **Note:** All API endpoints (except `/health`) require a PQ WebSocket session. The examples below show the JSON message format sent over the WebSocket connection — not direct HTTP requests. Authentication is via PQ identity keys established during the handshake. The `Authorization: Bearer` headers shown are **legacy placeholders** retained for readability — they are not used.
---
@@ -40,7 +38,7 @@ curl -H "Authorization: Bearer my-secret-key" \
"scanners/dir_brute",
"creds/generic/ssh_bruteforce"
],
- "count": 181,
+ "count": 240,
"request_id": "abc123",
"timestamp": "2026-03-17T14:01:00Z",
"duration_ms": 2
@@ -58,18 +56,6 @@ curl -H "Authorization: Bearer my-secret-key" \
---
-## Validate Parameters (Dry Run)
-
-```bash
-curl -X POST \
- -H "Authorization: Bearer my-secret-key" \
- -H "Content-Type: application/json" \
- -d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
- http://localhost:8080/api/validate
-```
-
----
-
## Run a Port Scan
```bash
@@ -198,60 +184,6 @@ curl -X POST \
---
-## Check Server Status & Statistics
-
-```bash
-curl -H "Authorization: Bearer my-secret-key" \
- http://localhost:8080/api/status
-```
-
-**Response:**
-```json
-{
- "uptime_seconds": 3600,
- "requests_total": 142,
- "auth_failures": 3,
- "tracked_ips": 2,
- "hardening_enabled": true,
- "ip_limit": 5,
- "request_id": "def456",
- "timestamp": "2026-03-17T15:00:00Z",
- "duration_ms": 1
-}
-```
-
----
-
-## View Tracked IPs
-
-```bash
-curl -H "Authorization: Bearer my-secret-key" \
- http://localhost:8080/api/ips
-```
-
----
-
-## View Auth Failure Stats
-
-```bash
-curl -H "Authorization: Bearer my-secret-key" \
- http://localhost:8080/api/auth-failures
-```
-
----
-
-## Manually Rotate API Key
-
-```bash
-curl -X POST \
- -H "Authorization: Bearer my-secret-key" \
- http://localhost:8080/api/rotate-key
-```
-
-The response includes the **new key** — store it immediately as the old key is invalidated.
-
----
-
## Global Options
```bash
diff --git a/docs/CLI-Reference.md b/docs/CLI-Reference.md
index 80760ce..11e54d9 100644
--- a/docs/CLI-Reference.md
+++ b/docs/CLI-Reference.md
@@ -36,7 +36,8 @@ An optional positional argument (`exploit`, `scanner`, `creds`) can be used to s
| `--list-modules` | | Print all available modules and exit |
| `--verbose` | `-v` | Enable detailed logging |
| `--output-format` | | Control output: `text` (default) or `json` |
-| `--api` | | Start the PQ-encrypted REST API server |
+| `--api` | | Start the PQ-encrypted REST + WebSocket API server |
+| `--mcp` | | Start as MCP (Model Context Protocol) server on stdio |
| `--interface ` | | Bind address for API server (default: `127.0.0.1:8080`) |
| `--pq-host-key ` | | PQ host key file (default: `~/.rustsploit/pq_host_key`) |
| `--pq-authorized-keys ` | | Authorized client keys file (default: `~/.rustsploit/pq_authorized_keys`) |
diff --git a/docs/Changelog.md b/docs/Changelog.md
index f6bc044..8411886 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -4,15 +4,63 @@ A high-level summary of significant changes. For the full detailed log, see [`ch
---
-## v0.4.8 (2026-04-03)
+## v0.4.8 (2026-04-19)
### Module Totals
-- **137 exploit modules** (24 with `check()`) — cameras, routers, network infrastructure, webapps, frameworks, SSH, DoS, crypto, FTP, IPMI, telnet, Bluetooth, VoIP, Windows, payload generators
-- **24 scanner modules**
-- **19 credential modules** — all with full mass scan support (random, CIDR, file, comma-separated targets)
+- **183 exploit modules** — cameras, routers, network infrastructure, webapps, frameworks, SSH, VNC, DoS, crypto, FTP, IPMI, telnet, Bluetooth, VoIP, Windows, payload generators, honeypot exploits (Cowrie, Dionaea, HoneyTrap, SNARE), WAF (SafeLine)
+- **27 scanner modules**
+- **29 credential modules** — all with full mass scan support (random, CIDR, file, comma-separated targets)
- **1 plugin module**
-- **181 total modules**
+- **240 total modules**
+
+### New in April 2026
+
+#### 46 New Exploit Modules
+
+| Category | Modules |
+|----------|---------|
+| Cowrie (SSH honeypot) | `ansi_log_injection`, `llm_prompt_injection`, `ssrf_ipv6` |
+| Dionaea (honeypot) | `mqtt_underflow`, `mssql_dos`, `mysql_sqli`, `tftp_crash` |
+| HoneyTrap (honeypot) | `docker_panic`, `ftp_panic` |
+| SafeLine (WAF) | `cookie_attributes`, `nginx_injection`, `no_auth_probe`, `pre_auth_tfa`, `session_secret_entropy`, `unauth_writes` |
+| Snare (honeypot) | `cookie_dos`, `tanner_version_mitm` |
+| VNC | `rfb`, `libvnc_checkrect_overflow`, `libvnc_tight_filtergradient`, `libvnc_ultrazip`, `libvnc_websocket_overflow`, `libvnc_zrle_tile`, `tigervnc_rre_overflow`, `tigervnc_timing_oracle`, `tightvnc_decompression_bomb`, `tightvnc_des_hardcoded_key`, `tightvnc_ft_path_traversal`, `tightvnc_predictable_challenge`, `tightvnc_rect_overflow`, `x11vnc_dns_injection`, `x11vnc_env_injection`, `x11vnc_unixpw_inject` |
+| SSH | `asyncssh_beginauthpass`, `libssh2_rogue_server`, `paramiko_authnonepass`, `paramiko_unknown_method` |
+| Frameworks | `apache_camel/cve_2025_27636_camel_header_injection`, `php/cve_2025_51373_php_rce` |
+| Network Infra | `commvault/cve_2025_34028_commvault_rce`, `kubernetes/cve_2025_1974_ingress_nginx_rce` |
+| WebApps | `misp_rce_cve_2025_27364`, `nextjs_middleware_bypass_cve_2025_29927`, `vite_path_traversal_cve_2025_30208`, `zimbra_sqli_auth_bypass_cve_2025_25064` |
+
+#### 3 New Scanner Modules
+
+- `proxy_scanner` — HTTP CONNECT, SOCKS4/5, transparent proxy discovery
+- `reflect_scanner` — UDP amplification vulnerability scanner (DNS, NTP, SSDP, Memcached)
+- `vuln_checker` — Fingerprint-based vulnerability scanner across all exploit modules
+
+#### 10 New Credential Modules
+
+`couchdb_bruteforce`, `elasticsearch_bruteforce`, `http_basic_bruteforce`, `imap_bruteforce`, `memcached_bruteforce`, `mysql_bruteforce`, `postgres_bruteforce`, `proxy_bruteforce`, `redis_bruteforce`, `vnc_bruteforce`
+
+#### Infrastructure
+
+- **WebSocket transport** (`src/ws.rs`) — PQ-encrypted WebSocket endpoint at `/pq/ws` with 100-connection cap and heartbeat
+- **Root privilege helper** (`src/utils/privilege.rs`) — `require_root()` for raw-socket modules (DoS, ping sweep, ICMP)
+- **Unified HTTP client** (`src/utils/network.rs`) — `build_http_client()` and `build_http_client_with(HttpClientOpts)` replacing hand-rolled clients in 50+ modules
+- **TCP connect helpers** — `tcp_connect_addr()`, `tcp_connect_str()`, `blocking_tcp_connect()`, `udp_bind()` centralizing socket creation
+- **MCP hardening** — `isolate_protocol_stdout()` prevents module println! from corrupting JSON-RPC; `MAX_LINE_BYTES` (1 MiB) caps; binary-safe reads
+- **Spool hardening** — `O_NOFOLLOW` flag, parent symlink check, lock-first file creation, `write_line()` returns Result
+- **build.rs** — `check_available()` dispatch for capability queries without a target; optimized regex compilation
+
+#### Module Audit
+
+Systematic quality pass across all 183 exploit modules:
+- Replaced `std::thread::sleep` with async alternatives in SSH and scanner modules
+- Migrated raw `TcpStream::connect` to `tcp_connect_addr()` framework utility
+- Standardized 50+ modules from hand-rolled `reqwest::Client::builder` to `build_http_client()`
+- Added `require_root()` checks to all raw-socket modules (DoS, ping sweep, ICMP flood)
+- Added `zeroize` crate for sensitive data cleanup
+
+---
### Highlights
diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md
index efc86ca..794db19 100644
--- a/docs/Getting-Started.md
+++ b/docs/Getting-Started.md
@@ -1,6 +1,6 @@
# Getting Started
-Rustsploit is a modular offensive tooling framework for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. It ships an interactive shell, a CLI runner, a REST API server, and an ever-growing library of exploits, scanners, and credential modules.
+Rustsploit is a modular offensive tooling framework for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. It ships an interactive shell, a CLI runner, a WebSocket API server with post-quantum encryption, and an ever-growing library of exploits, scanners, and credential modules.
---
@@ -10,23 +10,22 @@ Rustsploit is a modular offensive tooling framework for embedded targets, writte
**Debian / Ubuntu / Kali:**
```bash
-sudo apt update
-sudo apt install pkg-config libssl-dev rustc libdbus-1-dev
+sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev libdbus-1-dev cmake
```
**Arch Linux:**
```bash
-sudo pacman -S pkgconf openssl freerdp rustc
+sudo pacman -S base-devel pkgconf openssl dbus cmake
```
**Gentoo:**
```bash
-sudo emerge dev-libs/openssl dev-util/pkgconf net-misc/freerdp
+sudo emerge dev-libs/openssl dev-util/pkgconf sys-apps/dbus dev-build/cmake
```
**Fedora / RHEL:**
```bash
-sudo dnf install pkgconf-pkg-config openssl-devel freerdp rustc
+sudo dnf install gcc make pkgconf-pkg-config openssl-devel dbus-devel cmake
```
### Rust & Cargo
@@ -36,7 +35,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
-> The minimum supported Rust version tracks stable. Run `rustup update` to stay current.
+> Rust 1.85+ is required (edition 2024). Run `rustup update` to stay current.
---
diff --git a/docs/Home.md b/docs/Home.md
index 3741056..2dc6c77 100644
--- a/docs/Home.md
+++ b/docs/Home.md
@@ -13,13 +13,13 @@ Welcome to the Rustsploit documentation hub. Use the links below to navigate to
| [Getting Started](Getting-Started.md) | Installation, build, quick-start, Docker deployment |
| [Interactive Shell](Interactive-Shell.md) | Shell walkthrough, command palette, chaining, shortcuts |
| [CLI Reference](CLI-Reference.md) | Command-line flags, non-shell usage, output formats |
-| [API Server](API-Server.md) | REST API startup, endpoints, auth, rate limiting, hardening |
+| [API Server](API-Server.md) | WebSocket API, PQ encryption, endpoints, rate limiting |
| [API Usage Examples](API-Usage-Examples.md) | Practical curl workflows, request/response samples |
-| [Module Catalog](Module-Catalog.md) | All 181 modules by category — 137 exploits, 24 scanners, 19 creds, 1 plugin |
+| [Module Catalog](Module-Catalog.md) | All 240 modules by category — 183 exploits, 27 scanners, 29 creds, 1 plugin |
| [Module Development](Module-Development.md) | How to author new modules, lifecycle, dispatcher |
| [Security & Validation](Security-Validation.md) | Input validation constants, security patterns, honeypot detection |
-| [Credential Modules Guide](Credential-Modules-Guide.md) | Best practices for 19 cred modules — mass scan, cfg_prompt_*, concurrency |
-| [Exploit Modules Guide](Exploit-Modules-Guide.md) | Best practices for 137 exploit modules — multi-target, cfg_prompt_*, validation |
+| [Credential Modules Guide](Credential-Modules-Guide.md) | Best practices for 29 cred modules — mass scan, cfg_prompt_*, concurrency |
+| [Exploit Modules Guide](Exploit-Modules-Guide.md) | Best practices for 183 exploit modules — multi-target, cfg_prompt_*, validation |
| [Utilities & Helpers](Utilities-Helpers.md) | `utils.rs` public API, target normalization, honeypot check |
| [Testing & QA](Testing-QA.md) | Build checks (0 errors, 0 warnings), smoke tests, wordlist validation |
| [Changelog](Changelog.md) | Release notes and version history (current: v0.4.8) |
diff --git a/docs/MCP-Integration.md b/docs/MCP-Integration.md
new file mode 100644
index 0000000..301b684
--- /dev/null
+++ b/docs/MCP-Integration.md
@@ -0,0 +1,222 @@
+# MCP Integration
+
+Rustsploit includes a built-in MCP (Model Context Protocol) server that enables integration with Claude Desktop and other MCP-compatible clients. The server communicates via JSON-RPC 2.0 over stdio (stdin/stdout), with no network listener.
+
+---
+
+## Starting the MCP Server
+
+```bash
+cargo run -- --mcp
+```
+
+The server reads one JSON-RPC 2.0 request per line from stdin and writes one response per line to stdout. Diagnostic messages go to stderr.
+
+---
+
+## Protocol
+
+- **Transport**: Newline-delimited JSON over stdio
+- **Protocol version**: `2024-11-05`
+- **Capabilities**: `tools`, `resources`
+- **Server name**: `rustsploit-mcp`
+
+### Supported JSON-RPC Methods
+
+| Method | Type | Description |
+|--------|------|-------------|
+| `initialize` | Request | Capability negotiation handshake |
+| `initialized` | Notification | Client acknowledgement (no response) |
+| `tools/list` | Request | List all available tools |
+| `tools/call` | Request | Execute a tool by name |
+| `resources/list` | Request | List all available resources |
+| `resources/read` | Request | Read a resource by URI |
+
+---
+
+## Tools (42)
+
+### Module Tools
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_modules` | List all available modules, optionally filtered by category | -- |
+| `search_modules` | Search modules by keyword (case-insensitive substring match) | `query` |
+| `module_info` | Get metadata for a specific module (name, description, authors, references, rank) | `module_path` |
+| `check_module` | Run a non-destructive vulnerability check against a target | `module_path`, `target` |
+
+### Target Tools
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `set_target` | Set the global target (IP, hostname, CIDR, or comma-separated list) | `target` |
+| `get_target` | Get the current global target, size, and subnet status | -- |
+| `clear_target` | Clear the global target | -- |
+
+### Execution
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `run_module` | Execute a module against a target, returning captured output | `module_path`, `target` |
+
+Optional params for `run_module`: `port` (integer), `verbose` (boolean), `prompts` (object of key-value string overrides).
+
+### Credential Tools
+
+Credentials are per-workspace -- each workspace maintains its own credential store at `~/.rustsploit/workspaces/{name}_creds.json`. Switching workspaces via `switch_workspace` loads that workspace's credentials.
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_creds` | List all stored credentials in the current workspace | -- |
+| `search_creds` | Search credentials by host, service, or username in the current workspace | `query` |
+| `add_cred` | Add a credential to the current workspace's store | `host`, `username`, `secret` |
+| `delete_cred` | Delete a credential by its ID from the current workspace | `id` |
+
+Optional params for `add_cred`: `port` (integer), `service` (string), `cred_type` (password/hash/key/token).
+
+### Workspace Host and Service Tools
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_hosts` | List all tracked hosts in the current workspace | -- |
+| `add_host` | Add or update a host in the workspace | `ip` |
+| `delete_host` | Delete a host (and its services) from the workspace | `ip` |
+| `list_services` | List all tracked services in the current workspace | -- |
+| `add_service` | Add or update a service in the workspace | `host`, `port`, `service_name` |
+| `delete_service` | Delete a service by host and port | `host`, `port` |
+
+Optional params for `add_host`: `hostname`, `os_guess`. Optional params for `add_service`: `protocol` (default: tcp), `version`.
+
+### Loot Tools
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_loot` | List all stored loot entries | -- |
+| `search_loot` | Search loot by host, type, or description | `query` |
+| `add_loot` | Store a loot entry (text data) | `host`, `loot_type`, `data` |
+| `delete_loot` | Delete a loot entry by ID | `id` |
+
+Optional params for `add_loot`: `description`.
+
+### Global Options Tools
+
+Options are per-workspace -- they are scoped to the current workspace and stored at `~/.rustsploit/workspaces/{name}_options.json`. Switching workspaces via `switch_workspace` loads that workspace's options.
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_options` | List all persistent global options (setg values) for the current workspace | -- |
+| `set_option` | Set a persistent global option in the current workspace | `key`, `value` |
+| `unset_option` | Remove a persistent global option from the current workspace | `key` |
+
+### Job Tools
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_jobs` | List active background jobs | -- |
+| `kill_job` | Kill a background job by ID | `id` (integer) |
+
+### Workspace Management Tools
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `list_workspaces` | List all available workspaces | -- |
+| `switch_workspace` | Switch to a different workspace (creates if needed) | `name` |
+
+### Export
+
+| Tool | Description | Required Params |
+|------|-------------|-----------------|
+| `export_data` | Export full engagement data as JSON | -- |
+
+---
+
+## Resources (7)
+
+Resources provide read-only access to framework state.
+
+| URI | Name | Description | MIME Type |
+|-----|------|-------------|-----------|
+| `rustsploit:///modules` | Module Catalog | Full module list with `info()` metadata | `application/json` |
+| `rustsploit:///workspace` | Current Workspace | Tracked hosts and services | `application/json` |
+| `rustsploit:///credentials` | Credentials | Credential list with secrets redacted | `application/json` |
+| `rustsploit:///loot` | Loot Catalog | Loot metadata (no file content) | `application/json` |
+| `rustsploit:///options` | Global Options | Persistent setg key-value pairs | `application/json` |
+| `rustsploit:///target` | Current Target | Target value, size, and subnet flag | `application/json` |
+| `rustsploit:///status` | Framework Status | Module count, workspace, host/cred/loot counts | `application/json` |
+
+---
+
+## Claude Desktop Configuration
+
+Add the following to your `claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "rustsploit": {
+ "command": "/path/to/rustsploit",
+ "args": ["--mcp"]
+ }
+ }
+}
+```
+
+Replace `/path/to/rustsploit` with the absolute path to your compiled binary (e.g., `target/release/rustsploit`).
+
+---
+
+## Security
+
+- **Stdio transport only** -- no network listener, no authentication needed (single-user process)
+- **Target injection prevention** -- `run_module` strips any `target` key from the `prompts` object to prevent SSRF via prompt injection
+- **Module validation** -- module paths are verified against the build-time discovered module list before execution
+- **Credential redaction** -- the `rustsploit:///credentials` resource shows only the first 3 characters of each secret
+- **No file system writes** -- MCP tools return data inline; no direct file read/write operations are exposed
+- **Concurrency bounded** -- module execution is limited by the framework's semaphore (CPU count, minimum 4 concurrent)
+
+---
+
+## Architecture
+
+```
+src/mcp/
+ mod.rs -- Module re-exports
+ types.rs -- JSON-RPC 2.0 types, MCP capability structs, Tool/Resource/ToolResult types
+ server.rs -- Stdio event loop, request routing, response serialization
+ tools.rs -- 42 tool definitions and dispatch handlers
+ resources.rs -- 7 resource definitions and read handlers
+ client.rs -- MCP client implementation (for connecting to external MCP servers)
+```
+
+### Request Flow
+
+1. `server.rs` reads a JSON line from stdin
+2. Parses it as a `JsonRpcRequest`
+3. Routes by method name: `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`
+4. Handler extracts typed parameters from `params`
+5. Calls framework APIs (same functions used by the REST API and interactive shell)
+6. Returns a `JsonRpcResponse` serialized as a single JSON line on stdout
+
+---
+
+## Example Session
+
+```
+-> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
+<- {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{},"resources":{}},"serverInfo":{"name":"rustsploit-mcp","version":"0.4.8"}}}
+
+-> {"jsonrpc":"2.0","method":"initialized","params":{}}
+
+-> {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
+<- {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
+
+-> {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"set_target","arguments":{"target":"192.168.1.1"}}}
+<- {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Target set to: 192.168.1.1"}]}}
+
+-> {"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"rustsploit:///status"}}
+<- {"jsonrpc":"2.0","id":4,"result":{"uri":"rustsploit:///status","mimeType":"application/json","text":"{...}"}}
+```
+
+---
+
+> The MCP server uses the same framework internals as the REST API and interactive shell. Module execution, credential storage, workspace tracking, and all other operations produce identical results regardless of the interface used.
diff --git a/docs/Module-Catalog.md b/docs/Module-Catalog.md
index fdeb89b..ff989cd 100644
--- a/docs/Module-Catalog.md
+++ b/docs/Module-Catalog.md
@@ -4,7 +4,7 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
> **Module categories:** `exploits/`, `scanners/`, `creds/`, `plugins/` -- all auto-discovered at build time. Adding a new subdirectory under `src/modules/` automatically creates a new category.
-**Totals:** 137 exploit modules (24 with `check()`), 24 scanners, 19 credential modules, 1 plugin.
+**Totals:** 183 exploit modules, 27 scanners, 29 credential modules, 1 plugin.
---
@@ -27,6 +27,14 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/cameras/reolink/reolink_rce_cve_2019_11001` | Reolink camera authenticated OS command injection via TestEmail (CVE-2019-11001) |
| `exploits/cameras/uniview/uniview_nvr_pwd_disclosure` | Uniview NVR remote credential extraction and decoding |
+### Cowrie (SSH Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/cowrie/ansi_log_injection` | Injects ANSI/OSC escape sequences into cowrie session logs via unsanitized crontab arguments for terminal-level code execution on replay |
+| `exploits/cowrie/llm_prompt_injection` | Exploits cowrie LLM mode where attacker commands are concatenated into the system prompt, coercing the LLM to echo real configuration data |
+| `exploits/cowrie/ssrf_ipv6` | Bypasses cowrie SSRF blocklist via IPv6 addresses (fc00::/7, fe80::/10, ::ffff:0:0/96) and DNS-rebinding TOCTOU |
+
### Crypto
| Module Path | Description |
@@ -34,6 +42,15 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/crypto/geth_dos_cve_2026_22862` | Go-Ethereum ECIES panic DoS via malformed encrypted messages (CVE-2026-22862) |
| `exploits/crypto/heartbleed` | OpenSSL Heartbleed memory leak exploitation (CVE-2014-0160) |
+### Dionaea (Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/dionaea/mqtt_underflow` | Malformed MQTT PUBLISH with TopicLength exceeding MessageLength triggers parser desync/UnicodeDecodeError in dionaea |
+| `exploits/dionaea/mssql_dos` | Crafted TDS7 LOGIN7 packet with misaligned password slice triggers unhandled UnicodeDecodeError in dionaea MSSQL handler |
+| `exploits/dionaea/mysql_sqli` | MySQL COM_FIELD_LIST with SQLite injection in table name leaks dionaea internal DB schema |
+| `exploits/dionaea/tftp_crash` | Malformed TFTP RRQ without trailing NUL causes struct.error in dionaea options parser |
+
### DoS / Stress Testing
| Module Path | Description |
@@ -50,12 +67,14 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/dos/ssdp_amplification` | Spoofed SSDP M-SEARCH requests for ~30x amplification |
| `exploits/dos/syn_ack_flood` | SYN packets to reflectors with spoofed victim source IP for SYN-ACK reflection |
| `exploits/dos/tcp_connection_flood` | High-concurrency TCP connection flood with optional RST close and HTTP payload |
+| `exploits/dos/telnet_iac_flood` | Telnet IAC negotiation flood exploiting unbounded SB/SE parsing and rapid WILL/DO option cycling |
| `exploits/dos/udp_flood` | High-speed UDP flood with random, null, and pattern payload modes |
### Frameworks
| Module Path | Description |
|-------------|-------------|
+| `exploits/frameworks/apache_camel/cve_2025_27636_camel_header_injection` | Apache Camel < 4.10.2 HTTP header injection via Simple expression language for OS command execution (CVE-2025-27636) |
| `exploits/frameworks/apache_tomcat/catkiller_cve_2025_31650` | Apache Tomcat memory leak via invalid HTTP/2 priority headers (CVE-2025-31650) |
| `exploits/frameworks/apache_tomcat/cve_2025_24813_apache_tomcat_rce` | Apache Tomcat deserialization RCE (CVE-2025-24813) |
| `exploits/frameworks/apache_tomcat/cve_2025_24813_tomcat_put_rce` | Apache Tomcat unauthenticated RCE via partial PUT and Java deserialization (CVE-2025-24813) |
@@ -67,6 +86,7 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/frameworks/mongo/mongobleed` | MongoDB zlib decompression heap memory disclosure (CVE-2025-14847) |
| `exploits/frameworks/nginx/nginx_pwner` | Nginx misconfiguration scanner: alias traversal, CRLF injection, PHP detection, and more |
| `exploits/frameworks/php/cve_2024_4577` | PHP CGI argument injection on Windows XAMPP for RCE (CVE-2024-4577) |
+| `exploits/frameworks/php/cve_2025_51373_php_rce` | PHP CGI on Windows soft hyphen code-page conversion allows argument injection for auto_prepend_file RCE (CVE-2025-51373) |
| `exploits/frameworks/wsus/cve_2025_59287_wsus_rce` | Unauthenticated RCE in Windows Server Update Services (CVE-2025-59287) |
### FTP
@@ -76,12 +96,25 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/ftp/ftp_bounce_test` | FTP bounce attack test via PORT commands to third-party hosts |
| `exploits/ftp/pachev_ftp_path_traversal_1_0` | Directory traversal in Pachev FTP Server 1.0 to read files outside FTP root |
+### HoneyTrap (Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/honeytrap/docker_panic` | POST /v1.40/images/create without fromImage causes nil map panic in HoneyTrap Docker emulation — daemon exit |
+| `exploits/honeytrap/ftp_panic` | Malformed FTP PORT command with insufficient fields causes slice out-of-range panic in HoneyTrap — daemon exit |
+
### IPMI
| Module Path | Description |
|-------------|-------------|
| `exploits/ipmi/ipmi_enum_exploit` | IPMI enumeration with cipher 0 bypass, default credential brute force, and RAKP hash dumping |
+### Network Infrastructure -- Commvault
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/network_infra/commvault/cve_2025_34028_commvault_rce` | Commvault Command Center < 11.38.0 unauthenticated path traversal file upload to RCE (CVE-2025-34028) |
+
### Network Infrastructure -- Citrix
| Module Path | Description |
@@ -114,6 +147,12 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
|-------------|-------------|
| `exploits/network_infra/hpe/cve_2025_37164_hpe_oneview_rce` | Unauthenticated RCE via REST API command injection in HPE OneView (CVE-2025-37164) |
+### Network Infrastructure -- Kubernetes
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/network_infra/kubernetes/cve_2025_1974_ingress_nginx_rce` | ingress-nginx admission webhook config injection via annotations for arbitrary NGINX config, file read, and RCE (CVE-2025-1974) |
+
### Network Infrastructure -- Ivanti
| Module Path | Description |
@@ -241,14 +280,42 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
|-------------|-------------|
| `exploits/routers/zyxel/zyxel_cpe_ci_cve_2024_40890` | Zyxel legacy CPE unauthenticated HTTP command injection (CVE-2024-40890) |
+### Sample
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/sample_exploit` | Template exploit module demonstrating info(), check(), and run() with cfg_prompt integration |
+
+### SafeLine (WAF)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/safeline/cookie_attributes` | SafeLine session cookie lacks HttpOnly, Secure, and SameSite attributes enabling XSS session theft and CSRF |
+| `exploits/safeline/nginx_injection` | SafeLine tcontrollerd inserts Ports field verbatim into nginx config via fmt.Sprintf for arbitrary directive injection |
+| `exploits/safeline/no_auth_probe` | Detects SafeLine NO_AUTH env bypass where `len(noAuth) >= 0` (always true) disables auth middleware |
+| `exploits/safeline/pre_auth_tfa` | Fresh SafeLine install unauthenticated TFA secret rotation via /api/OTPUrl for full account takeover |
+| `exploits/safeline/session_secret_entropy` | SafeLine JWT signing secret generated with math/rand seeded by time.Now().UnixNano() — as low as 39 bits effective entropy |
+| `exploits/safeline/unauth_writes` | SafeLine publicRouters expose unauthenticated POST to /api/Behaviour and /api/FalsePositives for analytics pollution and request amplification |
+
+### Snare (Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/snare/cookie_dos` | HTTP Cookie header without '=' separator causes IndexError crash in snare tanner_handler.py worker |
+| `exploits/snare/tanner_version_mitm` | Rogue HTTP server on port 8090 returns forged version response to snare's unauthenticated GET /version check |
+
### SSH
| Module Path | Description |
|-------------|-------------|
+| `exploits/ssh/asyncssh_beginauthpass` | AsyncSSH server begin_auth() returning False causes USERAUTH_SUCCESS bypass for unauthenticated session access |
| `exploits/ssh/erlang_otp_ssh_rce_cve_2025_32433` | Erlang/OTP SSH server unauthenticated RCE (CVE-2025-32433) |
+| `exploits/ssh/libssh2_rogue_server` | Rogue SSH server capturing credentials from libssh2 clients that accept USERAUTH_SUCCESS without verifying KEX state |
| `exploits/ssh/libssh_auth_bypass_cve_2018_10933` | libSSH server authentication bypass (CVE-2018-10933) |
| `exploits/ssh/openssh_regresshion_cve_2024_6387` | OpenSSH sshd signal handler race condition for unauthenticated RCE (CVE-2024-6387) |
| `exploits/ssh/opensshserver_9_8p1race_condition` | OpenSSH 9.8p1 race condition for heap-based RCE |
+| `exploits/ssh/paramiko_authnonepass` | Paramiko SSH server check_auth_none() returning AUTH_SUCCESSFUL allows unauthenticated session access |
+| `exploits/ssh/paramiko_unknown_method` | Paramiko SSH server unrecognized auth method fallthrough to check_auth_none() allows authentication bypass |
| `exploits/ssh/sshpwn_auth_passwd` | OpenSSH auth2-passwd.c password length DoS, change info leak, timing enumeration |
| `exploits/ssh/sshpwn_pam` | OpenSSH auth-pam.c environment injection, memory leak DoS, username validation bypass |
| `exploits/ssh/sshpwn_scp_attacks` | OpenSSH SCP path traversal, command injection, and brace expansion DoS |
@@ -261,6 +328,27 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
|-------------|-------------|
| `exploits/telnet/telnet_auth_bypass_cve_2026_24061` | Telnet authentication bypass on vulnerable devices (CVE-2026-24061) |
+### VNC
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/vnc/libvnc_checkrect_overflow` | LibVNCClient signed 32-bit bounds check integer overflow for heap overflow RCE |
+| `exploits/vnc/libvnc_tight_filtergradient` | LibVNCClient Tight decoder unclamped numRows out-of-bounds write past allocated buffer |
+| `exploits/vnc/libvnc_ultrazip` | LibVNCClient Ultra encoding unbounded cache rect loop for heap overflow (CVE-2018-20750) |
+| `exploits/vnc/libvnc_websocket_overflow` | LibVNCServer WebSocket unbounded 64-bit payloadLen for heap overflow |
+| `exploits/vnc/libvnc_zrle_tile` | LibVNCClient ZRLE decoder truncated RLE tile buffer over-read |
+| `exploits/vnc/rfb` | Shared RFB protocol helpers for VNC exploit modules |
+| `exploits/vnc/tigervnc_rre_overflow` | TigerVNC RRE decoder unbounded numSubrects loop for heap over-read |
+| `exploits/vnc/tigervnc_timing_oracle` | TigerVNC VNC auth DES response timing side-channel for bit-by-bit key recovery |
+| `exploits/vnc/tightvnc_decompression_bomb` | TightVNC FileUploadData uncapped uncompressedSize for heap exhaustion DoS |
+| `exploits/vnc/tightvnc_des_hardcoded_key` | TightVNC hardcoded 8-byte DES key for offline Windows registry password decryption |
+| `exploits/vnc/tightvnc_ft_path_traversal` | TightVNC file-transfer handler directory traversal for arbitrary file read/write |
+| `exploits/vnc/tightvnc_predictable_challenge` | TightVNC srand(time(0)) predictable 16-byte RFB challenge for replay attacks |
+| `exploits/vnc/tightvnc_rect_overflow` | TightVNC signed int32 multiplication overflow in Rect::area() for heap buffer overflow RCE |
+| `exploits/vnc/x11vnc_dns_injection` | x11vnc reverse-DNS hostname passed unsanitized to system() for shell injection via crafted PTR record |
+| `exploits/vnc/x11vnc_env_injection` | x11vnc RFB_CLIENT_IP environment variable injection into hook scripts |
+| `exploits/vnc/x11vnc_unixpw_inject` | x11vnc -unixpw mode newline injection in plaintext username to confuse PAM flow |
+
### VoIP
| Module Path | Description |
@@ -278,8 +366,10 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/webapps/flowise/cve_2025_59528_flowise_rce` | Flowise < 3.0.5 unauthenticated API RCE (CVE-2025-59528) |
| `exploits/webapps/langflow_rce_cve_2025_3248` | Langflow unauthenticated RCE via Python exec() in code validation (CVE-2025-3248) |
| `exploits/webapps/laravel_livewire_rce_cve_2025_47949` | Laravel Livewire RCE via unsafe deserialization (CVE-2025-47949) |
+| `exploits/webapps/misp_rce_cve_2025_27364` | MISP < 2.5.3 authenticated file upload to PHP webshell RCE via /events/upload_sample (CVE-2025-27364) |
| `exploits/webapps/mcpjam/cve_2026_23744_mcpjam_rce` | MCPJam Inspector <= 1.4.2 unauthenticated RCE (CVE-2026-23744) |
| `exploits/webapps/n8n/n8n_rce_cve_2025_68613` | n8n workflow automation RCE via expression injection (CVE-2025-68613) |
+| `exploits/webapps/nextjs_middleware_bypass_cve_2025_29927` | Next.js < 15.2.3 middleware bypass via unauthenticated x-middleware-subrequest header (CVE-2025-29927) |
| `exploits/webapps/react/react2shell` | React Server Components / Next.js RCE via RSC Flight protocol deserialization |
| `exploits/webapps/roundcube/roundcube_postauth_rce` | Roundcube webmail post-auth RCE via deserialization in file upload |
| `exploits/webapps/sap_netweaver_rce_cve_2025_31324` | SAP NetWeaver Visual Composer unauthenticated file upload to RCE (CVE-2025-31324) |
@@ -288,12 +378,14 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/webapps/solarwinds/cve_2025_40551_solarwinds_whd_rce` | SolarWinds Web Help Desk unauthenticated Java deserialization RCE (CVE-2025-40551) |
| `exploits/webapps/spotube/spotube` | Spotube API path traversal via WebSocket and denial of service |
| `exploits/webapps/termix/termix_xss_cve_2026_22804` | Termix File Manager stored XSS via SVG upload in Electron context (CVE-2026-22804) |
+| `exploits/webapps/vite_path_traversal_cve_2025_30208` | Vite dev server < 6.2.3 /@fs/ path traversal via ?import&raw query parameter bypass (CVE-2025-30208) |
| `exploits/webapps/wordpress/vitepos_file_upload_cve_2025_13156` | Vitepos for WooCommerce authenticated arbitrary PHP file upload (CVE-2025-13156) |
| `exploits/webapps/wordpress/wp_bricks_rce_cve_2024_25600` | Bricks Builder for WordPress unauthenticated RCE via render_element (CVE-2024-25600) |
| `exploits/webapps/wordpress/wp_litespeed_rce_cve_2024_28000` | LiteSpeed Cache weak hash brute force for WordPress admin escalation (CVE-2024-28000) |
| `exploits/webapps/wordpress/wp_royal_elementor_rce_cve_2024_32suspended` | Royal Elementor Addons unauthenticated PHP webshell upload |
| `exploits/webapps/xwiki/cve_2025_24893_xwiki_rce` | XWiki SolrSearch unauthenticated RCE via Groovy template injection (CVE-2025-24893) |
| `exploits/webapps/zabbix/zabbix_7_0_0_sql_injection` | Zabbix 7.0.0 time-based SQL injection in API endpoints |
+| `exploits/webapps/zimbra_sqli_auth_bypass_cve_2025_25064` | Zimbra ZCS < 10.0.12 unauthenticated SQL injection via /service/home~ for email metadata extraction (CVE-2025-25064) |
### Windows
@@ -317,7 +409,9 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `scanners/nbns_scanner` | NBNS name queries to UDP 137 for Windows host discovery |
| `scanners/ping_sweep` | Host discovery via ICMP echo, TCP connect, SYN, and ACK probes with CIDR support |
| `scanners/port_scanner` | TCP/UDP port scanner with service detection, banner grabbing, and configurable ranges |
+| `scanners/proxy_scanner` | HTTP CONNECT, SOCKS4, SOCKS5, and transparent proxy discovery with authentication detection |
| `scanners/redis_scanner` | Redis instance discovery and unauthenticated access detection |
+| `scanners/reflect_scanner` | UDP amplification vulnerability scanner for DNS, NTP monlist, SSDP, and Memcached reflectors |
| `scanners/sample_scanner` | Demonstration scanner checking HTTP/HTTPS reachability and response codes |
| `scanners/sequential_fuzzer` | Character-based HTTP fuzzer with 10+ encodings, custom charsets, and concurrent requests |
| `scanners/service_scanner` | Service port banner grabbing and version identification |
@@ -330,6 +424,7 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `scanners/stalkroute_full_traceroute` | Advanced traceroute with ICMP/TCP/UDP probes, OS fingerprint spoofing, and decoy packets |
| `scanners/subdomain_scanner` | Subdomain brute-force enumeration via DNS resolution |
| `scanners/vnc_scanner` | VNC protocol version and security type enumeration |
+| `scanners/vuln_checker` | Fingerprint-based vulnerability scanner with detection signatures across all exploit modules |
| `scanners/waf_detector` | Web Application Firewall and CDN provider detection via HTTP response analysis |
---
@@ -340,23 +435,33 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| Module Path | Description |
|-------------|-------------|
-| `creds/generic/ssh_bruteforce` | SSH password brute force with default credential testing, combo mode, and subnet scanning |
-| `creds/generic/rdp_bruteforce` | RDP auth brute force with NLA, TLS, Standard RDP, and Negotiate security levels |
-| `creds/generic/ftp_bruteforce` | FTP/FTPS brute force with combo mode, concurrent connections, and subnet scanning |
-| `creds/generic/telnet_bruteforce` | Telnet brute force with full IAC negotiation, multiple attack modes, and subnet scanning |
-| `creds/generic/smtp_bruteforce` | SMTP auth brute force supporting PLAIN and LOGIN mechanisms with combo mode |
-| `creds/generic/pop3_bruteforce` | POP3/POP3S brute force with SSL/TLS support, retry logic, and subnet scanning |
-| `creds/generic/mqtt_bruteforce` | MQTT 3.1.1 auth testing with TLS/SSL, anonymous detection, and multiple attack modes |
-| `creds/generic/snmp_bruteforce` | SNMPv1/v2c community string brute force with read/write detection and subnet scanning |
-| `creds/generic/rtsp_bruteforce` | RTSP auth brute force for IP cameras with path bruting and custom headers |
-| `creds/generic/l2tp_bruteforce` | L2TP/IPsec VPN CHAP auth brute force against L2TP concentrators |
+| `creds/generic/couchdb_bruteforce` | CouchDB session cookie and HTTP Basic auth brute force with default credential testing and subnet scanning |
+| `creds/generic/elasticsearch_bruteforce` | Elasticsearch HTTP Basic auth brute force against cluster root and security API with subnet scanning |
+| `creds/generic/enablebruteforce` | Raises file descriptor limits (ulimit) for high-concurrency brute-force operations |
| `creds/generic/fortinet_bruteforce` | Fortinet FortiGate SSL VPN web auth brute force with certificate pinning and realm support |
| `creds/generic/ftp_anonymous` | FTP anonymous access check with FTPS, IPv4/IPv6, and mass scanning support |
-| `creds/generic/telnet_hose` | Mass internet Telnet default credential scanner with 500 workers and disk-based state |
-| `creds/generic/ssh_user_enum` | SSH username enumeration via timing-based side-channel attack (CVE-2018-15473 inspired) |
-| `creds/generic/ssh_spray` | SSH password spray across multiple targets with lockout-aware delays |
-| `creds/generic/enablebruteforce` | Raises file descriptor limits (ulimit) for high-concurrency brute-force operations |
+| `creds/generic/ftp_bruteforce` | FTP/FTPS brute force with combo mode, concurrent connections, and subnet scanning |
+| `creds/generic/http_basic_bruteforce` | HTTP Basic Authentication brute force with HTTPS support, default credentials, and subnet scanning |
+| `creds/generic/imap_bruteforce` | IMAP/IMAPS LOGIN command brute force over raw TCP with TLS support and subnet scanning |
+| `creds/generic/l2tp_bruteforce` | L2TP/IPsec VPN CHAP auth brute force against L2TP concentrators |
+| `creds/generic/memcached_bruteforce` | Memcached open instance detection and SASL PLAIN auth brute force over binary protocol |
+| `creds/generic/mqtt_bruteforce` | MQTT 3.1.1 auth testing with TLS/SSL, anonymous detection, and multiple attack modes |
+| `creds/generic/mysql_bruteforce` | MySQL native password wire protocol brute force with HandshakeV10 parsing and subnet scanning |
+| `creds/generic/pop3_bruteforce` | POP3/POP3S brute force with SSL/TLS support, retry logic, and subnet scanning |
+| `creds/generic/postgres_bruteforce` | PostgreSQL protocol v3 brute force supporting cleartext and MD5 auth with subnet scanning |
+| `creds/generic/proxy_bruteforce` | HTTP CONNECT, SOCKS5, and HTTP forward proxy authentication brute force |
+| `creds/generic/rdp_bruteforce` | RDP auth brute force with NLA, TLS, Standard RDP, and Negotiate security levels |
+| `creds/generic/redis_bruteforce` | Redis AUTH brute force supporting legacy and ACL mode with server info gathering on success |
+| `creds/generic/rtsp_bruteforce` | RTSP auth brute force for IP cameras with path bruting and custom headers |
| `creds/generic/sample_cred_check` | Sample module testing HTTP Basic Auth with default admin:admin credentials |
+| `creds/generic/smtp_bruteforce` | SMTP auth brute force supporting PLAIN and LOGIN mechanisms with combo mode |
+| `creds/generic/snmp_bruteforce` | SNMPv1/v2c community string brute force with read/write detection and subnet scanning |
+| `creds/generic/ssh_bruteforce` | SSH password brute force with default credential testing, combo mode, and subnet scanning |
+| `creds/generic/ssh_spray` | SSH password spray across multiple targets with lockout-aware delays |
+| `creds/generic/ssh_user_enum` | SSH username enumeration via timing-based side-channel attack (CVE-2018-15473 inspired) |
+| `creds/generic/telnet_bruteforce` | Telnet brute force with full IAC negotiation, multiple attack modes, and subnet scanning |
+| `creds/generic/telnet_hose` | Mass internet Telnet default credential scanner with 500 workers and disk-based state |
+| `creds/generic/vnc_bruteforce` | VNC DES challenge-response brute force with bit-reversed key derivation and subnet scanning |
### Camera
diff --git a/docs/Module-Development.md b/docs/Module-Development.md
index 04c5c39..c238fb1 100644
--- a/docs/Module-Development.md
+++ b/docs/Module-Development.md
@@ -42,7 +42,8 @@ rustsploit/
│ ├── main.rs # Entry point — CLI or shell mode, input validation
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers
-│ ├── api.rs # REST API server — auth, rate limiting, hardening
+│ ├── api.rs # REST + WebSocket API server — PQ encryption, rate limiting
+│ ├── ws.rs # PQ-encrypted WebSocket transport (/pq/ws)
│ ├── config.rs # Global config and target validation
│ ├── module_info.rs # ModuleInfo, CheckResult, ModuleRank types
│ ├── global_options.rs # Persistent global options (setg/unsetg)
@@ -52,19 +53,23 @@ rustsploit/
│ ├── loot.rs # Loot/evidence management
│ ├── export.rs # JSON/CSV/summary report export
│ ├── jobs.rs # Background job management
+│ ├── mcp/
+│ │ ├── mod.rs # MCP server entry point (--mcp flag)
+│ │ ├── server.rs # JSON-RPC stdio transport with binary-safe reads
+│ │ └── tools.rs # 38 MCP tool implementations
│ ├── commands/
│ │ ├── mod.rs # Module discovery, fuzzy matching, multi-target dispatch
│ │ ├── exploit.rs
│ │ ├── scanner.rs
│ │ └── creds.rs
│ ├── modules/
-│ │ ├── exploits/ # Exploit modules (137 modules, 24 with check)
-│ │ ├── scanners/ # Scanner modules (24 modules)
-│ │ ├── creds/ # Credential modules (19 modules)
+│ │ ├── exploits/ # Exploit modules (183 modules, 21 categories)
+│ │ ├── scanners/ # Scanner modules (27 modules)
+│ │ ├── creds/ # Credential modules (29 modules)
│ │ └── plugins/ # Plugin modules (1 module)
│ ├── native/ # Native integrations
│ │ ├── mod.rs
-│ │ ├── rdp.rs # xfreerdp/rdesktop wrapper
+│ │ ├── rdp.rs # Native RDP auth (X.224, TLS, CredSSP/NTLM)
│ │ ├── payload_engine.rs # Payload encoding/generation
│ │ ├── url_encoding.rs # URL encoding utilities
│ │ └── async_tls.rs # Async TLS helpers
@@ -73,7 +78,8 @@ rustsploit/
│ ├── prompt.rs # Config-aware prompts (cfg_prompt_*)
│ ├── sanitize.rs # Input validation, length limits
│ ├── target.rs # Target normalization (IPv4/IPv6/CIDR/hostname)
-│ ├── network.rs # Network utilities
+│ ├── network.rs # HTTP client builders, TCP/UDP connect helpers
+│ ├── privilege.rs # Root privilege check (require_root)
│ └── modules.rs # Module discovery helpers
├── docs/ # This wiki
├── lists/ # Wordlists and data files
diff --git a/docs/Security-Validation.md b/docs/Security-Validation.md
index 97513f0..c5e5e06 100644
--- a/docs/Security-Validation.md
+++ b/docs/Security-Validation.md
@@ -121,6 +121,42 @@ The API server (`api.rs`) implements:
- **Auto-cleanup** — old entries purged at 100,000 entries
- **IP tracking + key rotation** — suspicious activity triggers auto-rotation in hardening mode
- **Secure defaults** — by default, considers `127.0.0.1` as the intended private bind
+- **WebSocket limits** — max 100 concurrent connections, 1 MiB frame cap, 30s heartbeat
+
+---
+
+## MCP Server Security
+
+The MCP server (`mcp/server.rs`) implements:
+
+- **`isolate_protocol_stdout()`** — redirects fd 1 to /dev/null so module `println!` cannot corrupt the JSON-RPC stream
+- **`MAX_LINE_BYTES`** — 1 MiB cap on incoming lines to prevent memory exhaustion
+- **Binary-safe reads** — uses `read_until()` instead of `read_line()` for no UTF-8 requirement
+- **Non-UTF-8 error handling** — returns proper JSON-RPC error responses for malformed input
+
+---
+
+## Spool Security
+
+The spool system (`spool.rs`) implements:
+
+- **`O_NOFOLLOW`** — prevents TOCTOU race conditions on symlinked spool files
+- **Parent symlink check** — rejects spool paths with symlinked parent directories
+- **Lock-first pattern** — acquires write lock before creating files to prevent orphaned files
+- **`write_line()` returns `Result`** — callers handle write failures instead of silently dropping output
+
+---
+
+## Privilege Checks
+
+Modules requiring raw sockets call `require_root()` at startup:
+
+```rust
+use crate::utils::privilege::require_root;
+require_root("ICMP raw socket")?;
+```
+
+Returns a descriptive error with the current euid instead of a cryptic "permission denied" from the socket layer. Used by DoS modules, ping sweep, and raw packet scanners.
---
diff --git a/docs/Testing-QA.md b/docs/Testing-QA.md
index 4538be4..2cf1a66 100644
--- a/docs/Testing-QA.md
+++ b/docs/Testing-QA.md
@@ -19,7 +19,7 @@ cargo clippy
cargo check
```
-A clean `cargo check` with **0 errors and 0 warnings** is required. The current codebase (all 181 modules) passes this check cleanly.
+A clean `cargo check` with **0 errors and 0 warnings** is required. The current codebase (all 240 modules) passes this check cleanly.
---
@@ -29,7 +29,7 @@ A clean `cargo check` with **0 errors and 0 warnings** is required. The current
cargo build
```
-`build.rs` regenerates the dispatchers (`exploit_dispatch.rs`, `scanner_dispatch.rs`, `creds_dispatch.rs`, `plugins_dispatch.rs`, `module_registry.rs`) into `OUT_DIR` during compilation. All 181 modules (137 exploits, 24 scanners, 19 creds, 1 plugin) are auto-discovered and dispatched by `build.rs`. If a new module fails to register, ensure `pub mod your_module;` is present in the sibling `mod.rs`.
+`build.rs` regenerates the dispatchers (`exploit_dispatch.rs`, `scanner_dispatch.rs`, `creds_dispatch.rs`, `plugins_dispatch.rs`, `module_registry.rs`) into `OUT_DIR` during compilation. All 240 modules (183 exploits, 27 scanners, 29 creds, 1 plugin) are auto-discovered and dispatched by `build.rs`. If a new module fails to register, ensure `pub mod your_module;` is present in the sibling `mod.rs`.
---
@@ -48,7 +48,7 @@ go # Runs the sample scanner against localhost
### CLI
```bash
-cargo run -- --command scanner --module sample_scanner --target 127.0.0.1
+cargo run -- -m scanners/sample_scanner -t 127.0.0.1
cargo run -- --list-modules # Verify your module is listed
```
@@ -57,8 +57,8 @@ cargo run -- --list-modules # Verify your module is listed
# Start the server
cargo run -- --api
-# Check your module appears
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/modules | grep your_module
+# Verify server starts (module listing requires PQ WebSocket session)
+curl http://localhost:8080/health
```
---
@@ -127,18 +127,10 @@ export json /tmp/test.json # Should create JSON file
```
```bash
-# API smoke test
+# API smoke test — verify server starts and health endpoint responds
cargo run -- --api
-
-# New endpoints
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/options
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/creds
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/hosts
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/services
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/workspace
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/loot
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/jobs
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/export?format=json
+curl http://localhost:8080/health
+# All other endpoints require a PQ WebSocket session — see API-Server.md
```
---
diff --git a/docs/Utilities-Helpers.md b/docs/Utilities-Helpers.md
index 5f8c9b2..3f79e11 100644
--- a/docs/Utilities-Helpers.md
+++ b/docs/Utilities-Helpers.md
@@ -5,6 +5,8 @@ Rustsploit provides several utility modules that every module developer should k
| Module | Import Path | Purpose |
|--------|-------------|---------|
| **Core Utils** | `crate::utils` | Target normalization, file loading, config-aware prompts, input validation |
+| **Network Utils** | `crate::utils::network` | HTTP client builders, TCP/UDP connect helpers, honeypot check |
+| **Privilege Utils** | `crate::utils::privilege` | Root privilege check for raw-socket modules |
| **Creds Utils** | `crate::modules::creds::utils` | Bruteforce statistics, subnet helpers, IP exclusion, scan state tracking |
| **Config** | `crate::config` | Global target state, module config, API prompt keys, results directory |
| **Global Options** | `crate::global_options` | Persistent `setg` options — checked by `cfg_prompt_*` after custom_prompts |
@@ -619,6 +621,120 @@ std::fs::write(&out_path, results)?;
---
+## `crate::utils::network` — Network Utilities
+
+Import path:
+
+```rust
+use crate::utils::network::{
+ build_http_client, build_http_client_with, HttpClientOpts,
+ tcp_connect_addr, tcp_connect_str, tcp_connect, tcp_port_open,
+ blocking_tcp_connect, udp_bind, quick_honeypot_check,
+};
+```
+
+---
+
+### `build_http_client(timeout) → Result`
+
+Creates a standard `reqwest::Client` with sensible defaults (danger-accept invalid certs, no redirect limit). Use this instead of hand-rolling `reqwest::Client::builder()`.
+
+```rust
+use crate::utils::network::build_http_client;
+
+let client = build_http_client(Duration::from_secs(10))?;
+let resp = client.get(&url).send().await?;
+```
+
+---
+
+### `build_http_client_with(timeout, opts) → Result`
+
+Extended HTTP client builder with additional options.
+
+```rust
+use crate::utils::network::{build_http_client_with, HttpClientOpts};
+
+let client = build_http_client_with(Duration::from_secs(10), HttpClientOpts {
+ cookie_store: true,
+ follow_redirects: true,
+ user_agent: Some("Mozilla/5.0".to_string()),
+ ..HttpClientOpts::default()
+})?;
+```
+
+#### `HttpClientOpts` Fields
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `cookie_store` | `bool` | `false` | Enable cookie jar |
+| `follow_redirects` | `bool` | `false` | Follow HTTP redirects |
+| `user_agent` | `Option` | `None` | Custom User-Agent header |
+| `default_headers` | `Option` | `None` | Default headers for all requests |
+
+---
+
+### `tcp_connect_addr(addr, timeout) → io::Result`
+
+Async TCP connection to a `SocketAddr` with timeout and optional source port binding. Preferred over raw `TcpStream::connect` — respects global source port setting.
+
+```rust
+use crate::utils::network::tcp_connect_addr;
+
+let stream = tcp_connect_addr(addr, Duration::from_secs(5)).await?;
+```
+
+---
+
+### `tcp_connect_str(addr_str, timeout) → io::Result`
+
+Async TCP connection from a `"host:port"` string. Resolves DNS and connects.
+
+---
+
+### `tcp_port_open(ip, port, timeout) → bool`
+
+Quick async check if a TCP port is open.
+
+---
+
+### `blocking_tcp_connect(addr, timeout) → io::Result`
+
+Synchronous TCP connection for use in `spawn_blocking` contexts.
+
+---
+
+### `udp_bind(target_ip) → io::Result`
+
+Binds a UDP socket to the appropriate address family (IPv4 or IPv6) for the target.
+
+---
+
+### `quick_honeypot_check(ip) → bool`
+
+Fast honeypot detection — probes common ports and returns `true` if 11+ respond (likely honeypot).
+
+---
+
+## `crate::utils::privilege` — Privilege Checks
+
+### `require_root(context) → Result<()>`
+
+Call at the top of `run()` in modules that need raw sockets (ICMP, SYN scan, packet crafting). Returns a clean error message if the current euid is not root.
+
+```rust
+use crate::utils::privilege::require_root;
+
+pub async fn run(target: &str) -> Result<()> {
+ require_root("ICMP raw socket")?;
+ // ... raw socket operations ...
+}
+```
+
+Used by: DoS modules (icmp_flood, syn_ack_flood, null_syn_exhaustion, dns_amplification, etc.), ping_sweep scanner.
+
+---
+
## Extending Utils
Add new reusable helpers to `src/utils/` (the appropriate submodule: `prompt.rs`, `sanitize.rs`, `target.rs`, `network.rs`, or `modules.rs`), `creds/utils.rs`, or `config.rs` rather than copy-pasting into individual modules. Common candidates:
diff --git a/extra.txt b/extra.txt
deleted file mode 100644
index 3ea659d..0000000
--- a/extra.txt
+++ /dev/null
@@ -1,58 +0,0 @@
-Required Signature
-
-The module must contain this exact public async function:
-
-pub async fn run(target: &str) -> anyhow::Result<()>
-
-Or any variant like:
-
-pub async fn run(_target: &str) -> anyhow::Result<()>
-
-Or even:
-
-pub async fn run(host: &str) -> anyhow::Result<()>
-
-
-Refactor this module to work with the auto-dispatch system. Do not remove any functionality or features. Make sure it defines a pub async fn run(target: &str) -> Result<()> entry point that internally calls the correct logic. Rename any conflicting functions if needed, but preserve all capabilities and structure.
-
-
-Refactor this code to a Rust module so that it fully integrates into my RouterSploit-inspired Rust auto-dispatch framework.
-
- ✅ Preserve all functionality and existing logic — do not remove or simplify any capabilities.
-
- ✅ Ensure the module defines a pub async fn run(target: &str) -> Result<()> entry point.
-
- All internal logic must be routed through this function.
-
- ✅ If any internal function is named run and conflicts with the dispatch entry, rename it (e.g. to execute, exploit, etc.) — but do not change logic.
-
- ✅ The module must compile, follow anyhow::Result<()>, and use proper error propagation (? operator).
-
- ✅ Do not add placeholders, pseudocode, or stubs — this must be real working Rust code.
-
- ✅ Use async/await and retain all networking, parsing, and exploit behavior from the original logic.
-
- ✅ Keep the code idiomatic and modular — preserve structure, variable naming, and async HTTP usage.
-
- ✅ If necessary, clean up variable scoping or imports, but never remove real features.
-
- ✅ keep all comments from the orginal but add two / before comments
-
- ✅ only use the poc and it must be a 1 to 1 convertion
-
-Here is the original module that needs to be refactored:
-
-
-
-
-Strict Requirements:
-
- The code must be 100% pure Rust, fully compatible with Linux operating systems.
-
- The entire driver must use asynchronous Rust throughout (async/await and appropriate crates), enabling non-blocking, concurrent communication with multiple devices.
-
- Do not include any comments, explanations, docstrings, sample usage, placeholder code, TODOs, or example outputs. The output must be only the actual source code required for a complete and functional driver.
-
- The output must be a single, fully compilable Rust source file, containing all necessary use statements, async functions, modules, structs, enums, and logic to support end-to-end operation.
-
-
diff --git a/plans/compile-time-improvement-plan.md b/plans/compile-time-improvement-plan.md
new file mode 100644
index 0000000..c7ebe40
--- /dev/null
+++ b/plans/compile-time-improvement-plan.md
@@ -0,0 +1,155 @@
+# Plan: Improve Cargo Build/Run Compile Times
+
+## Context
+
+Clean build takes **14m 39s** (879s) across 431 compilation units. Incremental rebuilds are already fast (0.6s). The goal is to reduce clean/cold build times — critical for CI, fresh clones, and dependency updates.
+
+The biggest bottlenecks (from `cargo --timings`):
+- `rustsploit` final crate: **427s** (361 source files compiled as one unit)
+- `aws-lc-sys`: **317s** (C library build for rustls crypto — pulled by reqwest & rustls)
+- `dbus`: **116s** (solely from btleplug — used by 1 file)
+- `tokio`: **105s**
+- `darling_core` + `strum_macros`: **182s** (solely from ratatui — used by 1 file)
+- `regex-automata` (×2): **175s**
+- `clap_builder`: **76s**
+- `h2`: **71s**
+- `serde_derive` + `async-trait`: **135s**
+- `libssh2-sys`: **62s** (C library for ssh2)
+- `hickory-proto`: **60s** (used by 1 file)
+
+---
+
+## Changes (ordered by impact / risk)
+
+### 1. Configure lld linker
+**Savings: ~30-60s | Risk: None | Effort: 2 min**
+
+`lld` is installed at `/usr/bin/lld` but not configured. The default GNU `ld` is slow for a 110K-line binary.
+
+Create `.cargo/config.toml`:
+```toml
+[target.x86_64-unknown-linux-gnu]
+linker = "clang"
+rustflags = ["-C", "link-arg=-fuse-ld=lld"]
+```
+
+---
+
+### 2. Switch rustls crypto from aws-lc-rs to ring
+**Savings: ~250-280s | Risk: Low | Effort: 5 min**
+
+`aws-lc-sys` (317s) compiles a massive C library via cmake. It's pulled in because `rustls 0.23` defaults to `aws-lc-rs`. The `ring` backend is functionally equivalent and compiles in ~30-50s.
+
+`cargo tree -i aws-lc-sys` confirms the chain: `aws-lc-sys → aws-lc-rs → rustls → {reqwest, tokio-rustls, rustsploit}`.
+
+In `Cargo.toml`:
+```toml
+rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
+```
+
+No source code changes — `ring` and `aws-lc-rs` expose the same `rustls::crypto::CryptoProvider` API.
+
+**File:** `Cargo.toml` line 50
+
+---
+
+### 3. Feature-gate btleplug + ratatui + crossterm
+**Savings: ~300s | Risk: Medium | Effort: 30 min**
+
+These three crates are used by exactly **one file**: `src/modules/exploits/bluetooth/wpair.rs`. Their transitive cost:
+
+| Dep chain | Compile time |
+|-----------|-------------|
+| btleplug → dbus | 116s |
+| btleplug → async-trait | 68s |
+| ratatui → strum_macros | 85s |
+| ratatui → darling_core | 97s |
+| ratatui → ratatui-core | 42s |
+| crossterm | ~15s |
+
+Confirmed via `cargo tree -i dbus`, `cargo tree -i strum_macros`, `cargo tree -i darling_core` — all solely from btleplug/ratatui.
+
+**Changes:**
+
+`Cargo.toml` — add features section, make deps optional:
+```toml
+[features]
+default = []
+bluetooth = ["dep:btleplug", "dep:ratatui", "dep:crossterm"]
+```
+
+```toml
+btleplug = { version = "0.12", optional = true }
+ratatui = { version = "0.30", optional = true }
+crossterm = { version = "0.29", optional = true }
+```
+
+`src/modules/exploits/bluetooth/mod.rs` — gate the module:
+```rust
+#[cfg(feature = "bluetooth")]
+pub mod wpair;
+```
+
+`build.rs` — skip bluetooth dir when feature is absent. In `generate_dispatch()` (or the `find_modules` walk), check `env::var("CARGO_FEATURE_BLUETOOTH")` and skip paths containing `bluetooth/` when it's not set. This prevents the generated dispatch from referencing `wpair::run` when the module doesn't exist.
+
+When bluetooth is needed: `cargo build --features bluetooth` or `cargo run --features bluetooth`.
+
+---
+
+### 4. Clean up tokio feature flags
+**Savings: ~5-10s | Risk: None | Effort: 2 min**
+
+Current line is redundant — `"full"` already includes every named feature plus extras like `test-util`:
+```toml
+tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
+```
+
+Replace with only what's actually used:
+```toml
+tokio = { version = "1.51", features = ["rt-multi-thread", "macros", "net", "io-util", "io-std", "fs", "process", "sync", "time", "signal"] }
+```
+
+**File:** `Cargo.toml` line 20
+
+---
+
+### 5. Feature-gate hickory DNS
+**Savings: ~60s | Risk: Low | Effort: 15 min**
+
+`hickory-proto` (60s) + `hickory-client` are used by exactly **one file**: `src/modules/scanners/dns_recursion.rs`.
+
+```toml
+[features]
+dns = ["dep:hickory-client", "dep:hickory-proto"]
+```
+
+```toml
+hickory-client = { version = "0.25", optional = true }
+hickory-proto = { version = "0.25", optional = true }
+```
+
+Gate in the scanner's `mod.rs` with `#[cfg(feature = "dns")]` and update `build.rs` to skip the module when the feature is absent.
+
+---
+
+## Files to modify
+
+| File | Changes |
+|------|---------|
+| `.cargo/config.toml` | **Create** — lld linker config |
+| `Cargo.toml` | rustls features, optional deps, `[features]` section, tokio cleanup |
+| `build.rs` | Skip feature-gated module dirs during code generation |
+| `src/modules/exploits/bluetooth/mod.rs` | `#[cfg(feature = "bluetooth")]` gate |
+| Scanner mod.rs for dns_recursion | `#[cfg(feature = "dns")]` gate |
+
+---
+
+## Verification
+
+1. `cargo clean && cargo build --timings 2>&1` — compare total time to baseline 879s
+2. `cargo build --features bluetooth,dns --timings` — verify full build still works
+3. `cargo run -- --help` — verify binary starts correctly
+4. `cargo run` — enter shell, run a non-bluetooth module (e.g. `use scanners/port_scanner`, `set target 127.0.0.1`, `run`) to confirm dispatch works
+5. `cargo build --features bluetooth` — verify bluetooth module compiles and appears in `list modules`
+
+**Expected result:** Clean build drops from ~879s to ~250-350s (60-70% reduction), with changes 1-3 providing the bulk of the savings.
diff --git a/preview.png b/preview.png
index 6a083f0..ba4ae52 100644
Binary files a/preview.png and b/preview.png differ
diff --git a/src/api.rs b/src/api.rs
index c044a8b..689ddeb 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -1,789 +1,246 @@
+use std::net::SocketAddr;
+use std::sync::Arc;
+
use anyhow::{Context, Result};
use axum::{
- extract::{ConnectInfo, Request, State},
- http::{HeaderValue, StatusCode},
- middleware::Next,
- response::{IntoResponse, Response},
+ response::Json,
routing::{get, post},
- Json, Router,
+ Router,
};
-use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
-use std::net::SocketAddr;
-use std::sync::{Arc, Mutex};
-use std::time::Instant;
-use tower::ServiceBuilder;
-use tower_http::{
- limit::RequestBodyLimitLayer,
- trace::TraceLayer,
-};
-
use colored::*;
-use crate::commands;
-
-/// Maximum request body size (1MB)
-const MAX_REQUEST_BODY_SIZE: usize = 1024 * 1024;
-
-// ─── IP Whitelist ───────────────────────────────────────────────────
-
-/// Load IP whitelist from ~/.rustsploit/ip_whitelist.conf
-/// File format: one IP per line, '#' comments, blank lines ignored
-fn load_ip_whitelist() -> Vec {
- let path = dirs_path().join("ip_whitelist.conf");
- if !path.exists() {
- return Vec::new();
- }
- match std::fs::read_to_string(&path) {
- Ok(contents) => {
- contents
- .lines()
- .map(|l| l.trim())
- .filter(|l| !l.is_empty() && !l.starts_with('#'))
- .map(|l| l.to_string())
- .collect()
- }
- Err(e) => {
- tracing::warn!(path = %path.display(), error = %e, "Failed to read IP whitelist");
- Vec::new()
- }
- }
-}
-
-/// Get the rustsploit config directory (~/.rustsploit/)
-fn dirs_path() -> std::path::PathBuf {
- let home = home::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
- home.join(".rustsploit")
-}
-
-// ─── State ──────────────────────────────────────────────────────────
-
-#[derive(Clone)]
-pub struct ApiState {
- verbose: bool,
- /// Optional IP whitelist — if non-empty, only these IPs can access the API
- ip_whitelist: Arc>,
- /// Persistent module selection (mirrors ShellContext.current_module)
- current_module: Arc>>,
- /// Per-IP rate limiter: IP -> (request_count, window_start)
- rate_limiter: Arc>>,
- /// Limits concurrent module execution to avoid resource exhaustion.
- /// No longer serializes to 1 — per-task output capture allows concurrency.
- run_semaphore: Arc,
-}
-
-/// Max requests per IP per window
-const RATE_LIMIT_MAX_REQUESTS: u32 = 30;
-/// Rate limit window duration in seconds
-const RATE_LIMIT_WINDOW_SECS: u64 = 10;
-
-// ─── Request / Response Types ───────────────────────────────────────
-
-#[derive(Serialize, Deserialize)]
-pub struct ApiResponse {
- pub success: bool,
- pub message: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub data: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub error_code: Option,
-}
-
-fn ok_response(message: impl Into, data: Option) -> ApiResponse {
- ApiResponse {
- success: true,
- message: message.into(),
- data,
- error_code: None,
- }
-}
-
-fn err_response(message: impl Into, code: &str) -> ApiResponse {
- ApiResponse {
- success: false,
- message: message.into(),
- data: None,
- error_code: Some(code.to_string()),
- }
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct RunModuleRequest {
- pub module: String,
- pub target: String,
- // Optional module config fields (passed to modules like the CLI would)
- pub port: Option,
- pub username_wordlist: Option,
- pub password_wordlist: Option,
- pub concurrency: Option,
- pub stop_on_success: Option,
- pub save_results: Option,
- pub output_file: Option,
- pub verbose: Option,
- pub combo_mode: Option,
- /// Generic prompt overrides — any key/value pair that modules read via
- /// `cfg_prompt_*()`. Examples: {"mode": "1", "timeout": "5", "retries": "3"}
- pub prompts: Option>,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct SetTargetRequest {
- pub target: String,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct HoneypotCheckRequest {
- pub target: String,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct ShellRequest {
- /// Single command (backward compatible)
- pub command: Option,
- /// Array of commands for chaining (preferred, more secure than string splitting)
- pub commands: Option>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct ShellResult {
- command: String,
- success: bool,
- output: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- duration_ms: Option,
-}
+use tower::ServiceBuilder;
+use tower_http::trace::TraceLayer;
// ─── Validation Helpers ─────────────────────────────────────────────
-fn validate_module_name(module: &str) -> bool {
+pub(crate) fn validate_module_name(module: &str) -> bool {
!module.is_empty()
&& module.len() <= 256
&& module.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9' | '/' | '_' | '-'))
}
-fn validate_target(target: &str) -> bool {
+pub(crate) fn validate_target(target: &str) -> bool {
!target.is_empty() && target.len() <= 2048 && !target.chars().any(|c| c.is_control())
}
-/// Target blocking is disabled — all targets are allowed.
-/// This is a pentesting framework; operators are responsible for their own targeting.
-fn is_blocked_target(_target: &str) -> bool {
+pub(crate) fn is_blocked_target(target: &str) -> bool {
+ const MASS_SCAN_KEYWORDS: &[&str] = &["random", "0.0.0.0", "0.0.0.0/0"];
+ if MASS_SCAN_KEYWORDS.contains(&target) {
+ return false;
+ }
+
+ let lower = target.to_lowercase();
+
+ // Try proper URL parsing first — handles @, port, scheme correctly
+ let host_no_port = if let Ok(parsed) = url::Url::parse(&lower) {
+ // Percent-decode the host and check it
+ parsed.host_str().map(|h| {
+ percent_decode_host(h)
+ })
+ } else {
+ None
+ };
+
+ // If URL parsing succeeded, check the extracted host
+ if let Some(ref host) = host_no_port {
+ if check_blocked_hostname(host) {
+ return true;
+ }
+ if let Ok(ip) = host.parse::() {
+ return is_blocked_ip(ip);
+ }
+ }
+
+ // Fallback: manual extraction for bare hostnames/IPs (no scheme)
+ let host_part = lower
+ .strip_prefix("http://")
+ .or_else(|| lower.strip_prefix("https://"))
+ .or_else(|| lower.strip_prefix("ftp://"))
+ .or_else(|| lower.strip_prefix("gopher://"))
+ .unwrap_or(&lower);
+ // Strip userinfo (everything before @)
+ let host_part = if let Some(at_pos) = host_part.find('@') {
+ &host_part[at_pos + 1..]
+ } else {
+ host_part
+ };
+ let host_part = host_part.split('/').next().unwrap_or(host_part);
+ let host_part = host_part.split('?').next().unwrap_or(host_part);
+ let host_part = host_part.split('#').next().unwrap_or(host_part);
+
+ // Strip port from host
+ let bare_host: &str = if host_part.starts_with('[') {
+ host_part
+ .trim_start_matches('[')
+ .split(']')
+ .next()
+ .unwrap_or(host_part)
+ } else if host_part.matches(':').count() == 1 {
+ host_part.split(':').next().unwrap_or(host_part)
+ } else {
+ host_part
+ };
+
+ let decoded = percent_decode_host(bare_host);
+
+ if check_blocked_hostname(&decoded) {
+ return true;
+ }
+ if let Ok(ip) = decoded.parse::() {
+ return is_blocked_ip(ip);
+ }
+ if let Ok(ip) = bare_host.parse::() {
+ return is_blocked_ip(ip);
+ }
+
+ // Block file:// scheme entirely
+ if lower.starts_with("file://") {
+ return true;
+ }
+
false
}
-/// Check if exec input contains shell metacharacters that could enable injection.
-fn contains_shell_metacharacters(input: &str) -> bool {
- input.chars().any(|c| matches!(c, '&' | '|' | ';' | '`' | '$' | '>' | '<' | '\n' | '\r'))
+fn percent_decode_host(host: &str) -> String {
+ let mut result = String::with_capacity(host.len());
+ let bytes = host.as_bytes();
+ let mut i = 0;
+ while i < bytes.len() {
+ if bytes[i] == b'%' && i + 2 < bytes.len() {
+ if let (Some(hi), Some(lo)) = (
+ hex_val(bytes[i + 1]),
+ hex_val(bytes[i + 2]),
+ ) {
+ result.push((hi << 4 | lo) as char);
+ i += 3;
+ continue;
+ }
+ }
+ result.push(bytes[i] as char);
+ i += 1;
+ }
+ result
+}
+
+fn hex_val(b: u8) -> Option {
+ match b {
+ b'0'..=b'9' => Some(b - b'0'),
+ b'a'..=b'f' => Some(b - b'a' + 10),
+ b'A'..=b'F' => Some(b - b'A' + 10),
+ _ => None,
+ }
+}
+
+fn check_blocked_hostname(host: &str) -> bool {
+ const BLOCKED_HOST_SUFFIXES: &[&str] = &[
+ "metadata.google.internal",
+ "metadata.goog",
+ "metadata.internal",
+ "metadata.azure.com",
+ "metadata.oraclecloud.com",
+ ];
+ const BLOCKED_HOST_EXACT: &[&str] = &[
+ "metadata",
+ "instance-data",
+ ];
+ const BLOCKED_WILDCARD_SUFFIXES: &[&str] = &[
+ ".sslip.io",
+ ".nip.io",
+ ".xip.io",
+ ".traefik.me",
+ ".local.gd",
+ ];
+
+ for suffix in BLOCKED_HOST_SUFFIXES {
+ if host == *suffix || host.ends_with(&format!(".{}", suffix)) {
+ return true;
+ }
+ }
+ for exact in BLOCKED_HOST_EXACT {
+ if host == *exact {
+ return true;
+ }
+ }
+ for wildcard in BLOCKED_WILDCARD_SUFFIXES {
+ if host.ends_with(wildcard) {
+ return true;
+ }
+ }
+ false
+}
+
+fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
+ match ip {
+ std::net::IpAddr::V6(v6) => {
+ if v6.is_loopback() { return true; }
+ let segs = v6.segments();
+ if segs[0] == 0xfd00 && segs[1] == 0x0ec2 { return true; }
+ if segs[0] & 0xfe00 == 0xfc00 { return true; }
+ if segs[0] & 0xffc0 == 0xfe80 { return true; }
+ if let Some(v4) = v6.to_ipv4_mapped() {
+ return is_blocked_ipv4(v4);
+ }
+ false
+ }
+ std::net::IpAddr::V4(v4) => is_blocked_ipv4(v4),
+ }
+}
+
+fn is_blocked_ipv4(v4: std::net::Ipv4Addr) -> bool {
+ let o = v4.octets();
+ o == [0, 0, 0, 0]
+ || o[0] == 127
+ || o[0] == 10
+ || (o[0] == 172 && (o[1] & 0xf0) == 16)
+ || (o[0] == 192 && o[1] == 168)
+ || (o[0] == 169 && o[1] == 254)
+ || o == [168, 63, 129, 16]
+ || o == [100, 100, 100, 200]
+}
+
+pub(crate) async fn is_blocked_target_resolved(target: &str) -> bool {
+ const MASS_SCAN_KEYWORDS: &[&str] = &["random", "0.0.0.0", "0.0.0.0/0"];
+ if MASS_SCAN_KEYWORDS.contains(&target) {
+ return false;
+ }
+
+ if is_blocked_target(target) {
+ return true;
+ }
+ let lower = target.to_lowercase();
+ let host_part = lower
+ .strip_prefix("http://")
+ .or_else(|| lower.strip_prefix("https://"))
+ .unwrap_or(&lower);
+ let host_part = host_part.split('/').next().unwrap_or(host_part);
+ let lookup_addr = if host_part.contains(':') {
+ host_part.to_string()
+ } else {
+ format!("{}:80", host_part)
+ };
+ match tokio::time::timeout(
+ std::time::Duration::from_secs(5),
+ tokio::net::lookup_host(&lookup_addr),
+ ).await {
+ Ok(Ok(addrs)) => {
+ for addr in addrs {
+ if is_blocked_ip(addr.ip()) {
+ return true;
+ }
+ }
+ false
+ }
+ Ok(Err(_)) | Err(_) => true,
+ }
+}
+
+pub(crate) fn contains_shell_metacharacters(input: &str) -> bool {
+ input.chars().any(|c| matches!(c, '&' | '|' | ';' | '`' | '$' | '>' | '<' | '\n' | '\r' | '(' | ')' | '{' | '}'))
|| input.contains("$(")
|| input.contains("${")
}
-// Auth middleware removed — authentication is now via PQ handshake (SSH-style identity keys).
-// The PQ middleware in pq_middleware.rs handles decryption and verifies the session.
-// Unauthenticated requests without X-PQ-Session header are rejected by the PQ middleware.
-
-// ─── Endpoint Handlers ─────────────────────────────────────────────
-
-async fn health_check() -> Json {
- Json(ok_response("API is running", None))
-}
-
-/// GET /api/modules — list all modules (like CLI `modules`)
-async fn list_modules() -> Json {
- let modules = commands::discover_modules();
-
- // Group modules by category dynamically
- let mut by_category: std::collections::BTreeMap> = std::collections::BTreeMap::new();
- for module in &modules {
- let category = module.split('/').next().unwrap_or("other").to_string();
- by_category.entry(category).or_default().push(module.clone());
- }
-
- Json(ok_response(
- format!("{} modules available", modules.len()),
- Some(serde_json::json!({
- "total": modules.len(),
- "categories": commands::categories(),
- "modules": by_category,
- })),
- ))
-}
-
-/// GET /api/modules/search?q=keyword — search modules (like CLI `find`)
-async fn search_modules(
- axum::extract::Query(params): axum::extract::Query>,
-) -> Json {
- let keyword = params.get("q").map(|s| s.as_str()).unwrap_or("");
-
- if keyword.is_empty() {
- return Json(err_response(
- "Missing search query parameter 'q'",
- "INVALID_INPUT",
- ));
- }
-
- let modules = commands::discover_modules();
- let kw = keyword.to_lowercase();
- let matches: Vec = modules
- .into_iter()
- .filter(|m| m.to_lowercase().contains(&kw))
- .collect();
-
- Json(ok_response(
- format!("Found {} modules matching '{}'", matches.len(), keyword),
- Some(serde_json::json!({ "modules": matches })),
- ))
-}
-
-/// GET /api/module/*path — check if module exists + return metadata
-/// Accepts any depth: /api/module/scanners/port_scanner or /api/module/creds/generic/ftp_anonymous
-async fn get_module_info(
- axum::extract::Path(module_path): axum::extract::Path,
-) -> Response {
- let module_path = module_path.trim_matches('/').to_string();
- let category = module_path.split('/').next().unwrap_or("").to_string();
- let name = module_path.split('/').last().unwrap_or("").to_string();
-
- if commands::discover_modules().contains(&module_path) {
- let info = commands::module_info(&module_path);
- let info_data = info.map(|i| serde_json::to_value(&i).ok()).flatten();
- (
- StatusCode::OK,
- Json(ok_response(
- "Module found",
- Some(serde_json::json!({
- "module": module_path,
- "category": category,
- "name": name,
- "exists": true,
- "info": info_data,
- })),
- )),
- )
- .into_response()
- } else {
- (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Module '{}' not found", module_path),
- "INVALID_MODULE",
- )),
- )
- .into_response()
- }
-}
-
-// ─── Global Options API ────────────────────────────────────────────
-
-/// GET /api/options — list all global options
-async fn get_options() -> Json {
- let opts = crate::global_options::GLOBAL_OPTIONS.all().await;
- Json(ok_response(
- format!("{} global options", opts.len()),
- Some(serde_json::json!({ "options": opts })),
- ))
-}
-
-/// POST /api/options — set a global option
-async fn set_option(Json(payload): Json>) -> Json {
- let mut set_count = 0;
- for (key, value) in &payload {
- crate::global_options::GLOBAL_OPTIONS.set(key, value).await;
- set_count += 1;
- }
- Json(ok_response(format!("{} option(s) set", set_count), None))
-}
-
-/// DELETE /api/options — clear a specific option (via query param or body)
-async fn delete_option(Json(payload): Json>) -> Json {
- let mut removed = 0;
- for key in payload.keys() {
- if crate::global_options::GLOBAL_OPTIONS.unset(key).await {
- removed += 1;
- }
- }
- Json(ok_response(format!("{} option(s) removed", removed), None))
-}
-
-// ─── Credential Store API ──────────────────────────────────────────
-
-/// GET /api/creds — list all credentials
-async fn list_creds() -> Json {
- tracing::info!("API: credentials listed");
- let creds = crate::cred_store::CRED_STORE.list().await;
- Json(ok_response(
- format!("{} credentials", creds.len()),
- Some(serde_json::to_value(&creds).unwrap_or_default()),
- ))
-}
-
-/// POST /api/creds — add a credential
-async fn add_cred(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let port = payload.get("port").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
- let service = payload.get("service").and_then(|v| v.as_str()).unwrap_or("unknown");
- let username = payload.get("username").and_then(|v| v.as_str()).unwrap_or("");
- let secret = payload.get("secret").and_then(|v| v.as_str()).unwrap_or("");
- let cred_type_str = payload.get("cred_type").and_then(|v| v.as_str()).unwrap_or("password");
- let source = payload.get("source_module").and_then(|v| v.as_str()).unwrap_or("api");
-
- if host.is_empty() || username.is_empty() {
- return Json(err_response("host and username are required", "INVALID_INPUT"));
- }
-
- let cred_type = match cred_type_str {
- "hash" => crate::cred_store::CredType::Hash,
- "key" => crate::cred_store::CredType::Key,
- "token" => crate::cred_store::CredType::Token,
- _ => crate::cred_store::CredType::Password,
- };
-
- let id = crate::cred_store::CRED_STORE.add(host, port, service, username, secret, cred_type, source).await;
- tracing::info!(host = %host, service = %service, "API: credential added");
- Json(ok_response("Credential added", Some(serde_json::json!({ "id": id }))))
-}
-
-/// DELETE /api/creds — delete a credential by ID
-async fn delete_cred(Json(payload): Json) -> Json {
- let id = payload.get("id").and_then(|v| v.as_str()).unwrap_or("");
- if crate::cred_store::CRED_STORE.delete(id).await {
- tracing::info!(id = %id, "API: credential deleted");
- Json(ok_response("Credential deleted", None))
- } else {
- Json(err_response("Credential not found", "NOT_FOUND"))
- }
-}
-
-// ─── Workspace API ─────────────────────────────────────────────────
-
-/// GET /api/hosts — list tracked hosts
-async fn list_hosts() -> Json {
- let hosts = crate::workspace::WORKSPACE.hosts().await;
- Json(ok_response(
- format!("{} hosts", hosts.len()),
- Some(serde_json::to_value(&hosts).unwrap_or_default()),
- ))
-}
-
-/// POST /api/hosts — add a host
-async fn add_host(Json(payload): Json) -> Json {
- let ip = payload.get("ip").and_then(|v| v.as_str()).unwrap_or("");
- if ip.is_empty() {
- return Json(err_response("ip is required", "INVALID_INPUT"));
- }
- let hostname = payload.get("hostname").and_then(|v| v.as_str());
- let os_guess = payload.get("os_guess").and_then(|v| v.as_str());
- crate::workspace::WORKSPACE.add_host(ip, hostname, os_guess).await;
- Json(ok_response("Host added", None))
-}
-
-/// GET /api/services — list tracked services
-async fn list_services() -> Json {
- let services = crate::workspace::WORKSPACE.services().await;
- Json(ok_response(
- format!("{} services", services.len()),
- Some(serde_json::to_value(&services).unwrap_or_default()),
- ))
-}
-
-/// POST /api/services — add a service
-async fn add_service(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let port = payload.get("port").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
- let protocol = payload.get("protocol").and_then(|v| v.as_str()).unwrap_or("tcp");
- let service_name = payload.get("service_name").and_then(|v| v.as_str()).unwrap_or("unknown");
- let version = payload.get("version").and_then(|v| v.as_str());
-
- if host.is_empty() || port == 0 {
- return Json(err_response("host and port are required", "INVALID_INPUT"));
- }
- crate::workspace::WORKSPACE.add_service(host, port, protocol, service_name, version).await;
- Json(ok_response("Service added", None))
-}
-
-/// GET /api/workspace — show current workspace info
-async fn get_workspace() -> Json {
- let name = crate::workspace::WORKSPACE.current_name().await;
- let workspaces = crate::workspace::WORKSPACE.list_workspaces().await;
- Json(ok_response("Workspace info", Some(serde_json::json!({
- "current": name,
- "available": workspaces,
- }))))
-}
-
-/// POST /api/workspace — switch workspace
-async fn switch_workspace(Json(payload): Json) -> Json {
- let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("");
- if name.is_empty() {
- return Json(err_response("name is required", "INVALID_INPUT"));
- }
- crate::workspace::WORKSPACE.switch(name).await;
- Json(ok_response(format!("Switched to workspace '{}'", name), None))
-}
-
-// ─── Loot API ──────────────────────────────────────────────────────
-
-/// GET /api/loot — list loot entries
-async fn list_loot() -> Json {
- let loot = crate::loot::LOOT_STORE.list().await;
- Json(ok_response(
- format!("{} loot items", loot.len()),
- Some(serde_json::to_value(&loot).unwrap_or_default()),
- ))
-}
-
-/// POST /api/loot — add loot
-async fn add_loot(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let loot_type = payload.get("loot_type").and_then(|v| v.as_str()).unwrap_or("other");
- let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("");
- let data = payload.get("data").and_then(|v| v.as_str()).unwrap_or("");
- let source = payload.get("source_module").and_then(|v| v.as_str()).unwrap_or("api");
-
- if host.is_empty() {
- return Json(err_response("host is required", "INVALID_INPUT"));
- }
-
- match crate::loot::LOOT_STORE.add_text(host, loot_type, description, data, source).await {
- Some(id) => Json(ok_response("Loot stored", Some(serde_json::json!({ "id": id })))),
- None => Json(err_response("Failed to store loot", "INTERNAL_ERROR")),
- }
-}
-
-// ─── Jobs API ──────────────────────────────────────────────────────
-
-/// GET /api/jobs — list background jobs
-async fn list_jobs() -> Json {
- let jobs = crate::jobs::JOB_MANAGER.list();
- let job_data: Vec = jobs.iter().map(|(id, module, target, started, status)| {
- serde_json::json!({
- "id": id,
- "module": module,
- "target": target,
- "started": started,
- "status": status,
- })
- }).collect();
- Json(ok_response(
- format!("{} jobs", job_data.len()),
- Some(serde_json::json!({ "jobs": job_data })),
- ))
-}
-
-/// DELETE /api/jobs/{id} — kill a job
-async fn kill_job(axum::extract::Path(id): axum::extract::Path) -> Json {
- if crate::jobs::JOB_MANAGER.kill(id) {
- Json(ok_response(format!("Job {} cancelled", id), None))
- } else {
- Json(err_response(format!("Job {} not found", id), "NOT_FOUND"))
- }
-}
-
-// ─── Export API ────────────────────────────────────────────────────
-
-/// GET /api/export — export engagement data
-async fn export_data(
- axum::extract::Query(params): axum::extract::Query>,
-) -> Response {
- let format = params.get("format").map(|s| s.as_str()).unwrap_or("json");
-
- // Gather data inline for API response
- let loot_entries = crate::loot::LOOT_STORE.list().await;
- let data = serde_json::json!({
- "workspace": crate::workspace::WORKSPACE.current_name().await,
- "exported_at": chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
- "hosts": crate::workspace::WORKSPACE.hosts().await,
- "services": crate::workspace::WORKSPACE.services().await,
- "credentials": crate::cred_store::CRED_STORE.list().await,
- "loot": loot_entries,
- });
-
- match format {
- "json" => (
- StatusCode::OK,
- Json(ok_response("Export complete", Some(data))),
- ).into_response(),
- _ => (
- StatusCode::BAD_REQUEST,
- Json(err_response("Use format=json for API export", "INVALID_INPUT")),
- ).into_response(),
- }
-}
-
-/// GET /api/target — show current target (like CLI `show_target`)
-async fn get_target() -> Json {
- let target = crate::config::GLOBAL_CONFIG.get_target();
- let size = crate::config::GLOBAL_CONFIG.get_target_size();
- let is_subnet = crate::config::GLOBAL_CONFIG.is_subnet();
-
- Json(ok_response(
- if target.is_some() {
- "Target retrieved"
- } else {
- "No target set"
- },
- Some(serde_json::json!({
- "target": target,
- "size": size,
- "is_subnet": is_subnet,
- })),
- ))
-}
-
-/// POST /api/target — set target (like CLI `set target `)
-async fn set_target(Json(payload): Json) -> Response {
- let target_raw = payload.target.as_str();
-
- if !validate_target(target_raw) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid target format", "INVALID_TARGET")),
- )
- .into_response();
- }
-
- match crate::config::GLOBAL_CONFIG.set_target(target_raw) {
- Ok(_) => {
- let is_subnet = crate::config::GLOBAL_CONFIG.is_subnet();
- let size = crate::config::GLOBAL_CONFIG.get_target_size();
- (
- StatusCode::OK,
- Json(ok_response(
- format!("Target set to '{}'", target_raw),
- Some(serde_json::json!({
- "target": target_raw,
- "is_subnet": is_subnet,
- "size": size,
- })),
- )),
- )
- .into_response()
- }
- Err(e) => (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- format!("Failed to set target: {}", e),
- "INVALID_TARGET",
- )),
- )
- .into_response(),
- }
-}
-
-/// DELETE /api/target — clear target (like CLI `clear_target`)
-async fn clear_target() -> Json {
- crate::config::GLOBAL_CONFIG.clear_target();
- Json(ok_response("Target cleared", None))
-}
-
-/// POST /api/run — run module synchronously (like CLI `run`)
-///
-/// This mirrors the CLI: calls `commands::run_module()` directly,
-/// captures stdout/stderr, and returns the output in the response.
-async fn run_module(
- State(state): State,
- Json(payload): Json,
-) -> Response {
- let start = std::time::Instant::now();
- let module_name = payload.module.as_str();
- let target_raw = payload.target.as_str();
-
- // Validate inputs
- if !validate_module_name(module_name) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Invalid module name format",
- "INVALID_MODULE",
- )),
- )
- .into_response();
- }
- if !validate_target(target_raw) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid target format", "INVALID_TARGET")),
- )
- .into_response();
- }
- // Check module exists
- if !commands::discover_modules().contains(&module_name.to_string()) {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Module '{}' not found", module_name),
- "INVALID_MODULE",
- )),
- )
- .into_response();
- }
-
- // Validate output_file for path traversal before injecting into prompts
- if let Some(ref f) = payload.output_file {
- let bad = f.contains("..") || f.contains('/') || f.contains('\\') || f.contains('\0');
- if bad {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Invalid output_file: must not contain path separators or traversal sequences",
- "INVALID_OUTPUT_FILE",
- )),
- ).into_response();
- }
- }
-
- // Set module config (like CLI does via prompts, the API passes them in the request)
- let mut module_config = crate::config::ModuleConfig {
- port: payload.port,
- username_wordlist: payload.username_wordlist.clone(),
- password_wordlist: payload.password_wordlist.clone(),
- concurrency: payload.concurrency,
- stop_on_success: payload.stop_on_success,
- save_results: payload.save_results,
- output_file: payload.output_file.clone(),
- verbose: payload.verbose,
- combo_mode: payload.combo_mode,
- custom_prompts: payload.prompts.clone().unwrap_or_default(),
- api_mode: true,
- };
-
- // Inject dedicated fields into custom_prompts so cfg_prompt_* picks them up.
- // Only insert if not already set by the explicit prompts map.
- if let Some(v) = module_config.port {
- module_config.custom_prompts.entry("port".into())
- .or_insert(v.to_string());
- }
- if let Some(ref v) = module_config.username_wordlist {
- module_config.custom_prompts.entry("username_wordlist".into())
- .or_insert(v.clone());
- }
- if let Some(ref v) = module_config.password_wordlist {
- module_config.custom_prompts.entry("password_wordlist".into())
- .or_insert(v.clone());
- }
- if let Some(v) = module_config.concurrency {
- module_config.custom_prompts.entry("concurrency".into())
- .or_insert(v.to_string());
- }
- if let Some(v) = module_config.save_results {
- module_config.custom_prompts.entry("save_results".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(v) = module_config.verbose {
- module_config.custom_prompts.entry("verbose".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(v) = module_config.stop_on_success {
- module_config.custom_prompts.entry("stop_on_success".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(v) = module_config.combo_mode {
- module_config.custom_prompts.entry("combo_mode".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(ref v) = module_config.output_file {
- module_config.custom_prompts.entry("output_file".into())
- .or_insert(v.clone());
- }
-
- // Strip "target" from custom_prompts to prevent SSRF bypass via prompt injection.
- // The validated target is passed directly to run_module() — modules should NOT
- // read a different target from custom_prompts.
- module_config.custom_prompts.remove("target");
-
- let verbose = state.verbose || payload.verbose.unwrap_or(false);
-
- if state.verbose {
- tracing::info!(
- module = module_name,
- target = target_raw,
- verbose,
- "Running module via API"
- );
- }
-
- tracing::info!(module = %module_name, target = %target_raw, "API: dispatching module");
-
- // Acquire concurrency permit to avoid resource exhaustion
- let _permit = match state.run_semaphore.acquire().await {
- Ok(permit) => permit,
- Err(_) => {
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(err_response(
- "Server is shutting down",
- "SERVICE_UNAVAILABLE",
- )),
- )
- .into_response();
- }
- };
-
- // Per-task output capture — no process-global gag, no serialization needed.
- // Each API request gets its own OutputBuffer via task-local storage,
- // so multiple modules can run concurrently without output interleaving.
- let output_buf = crate::output::OutputBuffer::new();
- let buf_clone = output_buf.clone();
-
- // Run inside a task-local RunContext so cfg_prompt_* reads per-request
- // config instead of the process-global MODULE_CONFIG.
- let (result, run_ctx) = crate::context::run_with_context_target(module_config, target_raw.to_string(), || async {
- crate::output::OUTPUT_BUFFER.scope(buf_clone, async {
- commands::run_module(module_name, target_raw, verbose).await
- }).await
- }).await;
-
- let duration_ms = start.elapsed().as_millis().min(u64::MAX as u128) as u64;
- let stdout_output = output_buf.drain_stdout();
- let stderr_output = output_buf.drain_stderr();
- let module_output = run_ctx.output.take();
-
- // Truncate output to prevent huge responses (max 64KB)
- let max_output = 64 * 1024;
- let output_truncated = if stdout_output.len() > max_output {
- format!("{}\n... (output truncated at {} bytes)", &stdout_output[..max_output], stdout_output.len())
- } else {
- stdout_output
- };
-
- match result {
- Ok(_) => (
- StatusCode::OK,
- Json(ok_response(
- format!(
- "Module '{}' executed successfully against '{}'",
- module_name, target_raw
- ),
- Some(serde_json::json!({
- "module": module_name,
- "target": target_raw,
- "status": "completed",
- "duration_ms": duration_ms,
- "output": output_truncated,
- "stderr": stderr_output,
- "findings": module_output.findings,
- })),
- )),
- )
- .into_response(),
- Err(e) => (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(err_response(
- format!(
- "Module '{}' failed on '{}': {}",
- module_name, target_raw, e
- ),
- "EXECUTION_ERROR",
- )),
- )
- .into_response(),
- }
-}
-
-// ─── Results File Retrieval ─────────────────────────────────────────
-
-/// Validate a result filename: ASCII-only, no path components, safe characters, .txt only.
-fn validate_result_filename(name: &str) -> bool {
+pub(crate) fn validate_result_filename(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 255
&& name.is_ascii()
@@ -795,1911 +252,89 @@ fn validate_result_filename(name: &str) -> bool {
&& name.ends_with(".txt")
}
-/// GET /api/results — list saved .txt result files in the results directory
-/// Only regular .txt files are listed; symlinks, hidden files, and non-.txt files are excluded.
-async fn list_results() -> Json {
- let results_dir = crate::config::results_dir();
- let mut files: Vec = Vec::new();
+// ─── Health Endpoint ────────────────────────────────────────────────
- if let Ok(entries) = std::fs::read_dir(&results_dir) {
- for entry in entries.flatten() {
- // Use symlink_metadata to detect symlinks (metadata() follows symlinks)
- let symlink_meta = match entry.path().symlink_metadata() {
- Ok(m) => m,
- Err(_) => continue,
- };
- // Skip symlinks entirely — prevent symlink-based path escapes
- if symlink_meta.file_type().is_symlink() {
- continue;
- }
- if let Ok(meta) = entry.metadata() {
- if meta.is_file() {
- let name = entry.file_name().to_string_lossy().to_string();
- // Only list .txt files, ASCII names, no hidden files
- if name.is_ascii()
- && name.ends_with(".txt")
- && !name.starts_with('.')
- {
- files.push(serde_json::json!({
- "name": name,
- "size_bytes": meta.len(),
- "modified": meta.modified().ok()
- .and_then(|t| t.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
- .map(|d| d.as_secs()),
- }));
- }
- }
- }
- }
- }
-
- files.sort_by(|a, b| {
- let a_name = a["name"].as_str().unwrap_or("");
- let b_name = b["name"].as_str().unwrap_or("");
- a_name.cmp(b_name)
- });
-
- Json(ok_response(
- format!("{} result file(s) found", files.len()),
- Some(serde_json::json!({
- "files": files,
- })),
- ))
-}
-
-/// GET /api/results/{filename} — retrieve a specific saved .txt result file
-/// Filename must be ASCII-only, end with .txt, no path separators, safe characters only.
-/// Returns the file content as plain text within a JSON envelope.
-async fn get_result_file(
- axum::extract::Path(filename): axum::extract::Path,
-) -> Response {
- // validate_result_filename enforces .txt extension, ASCII-only, no path separators, etc.
- if !validate_result_filename(&filename) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Invalid filename. Must be ASCII-only, end with .txt, no path separators, only alphanumeric/underscore/dash/dot.",
- "INVALID_FILENAME",
- )),
- )
- .into_response();
- }
-
- let results_dir = crate::config::results_dir();
- let file_path = results_dir.join(&filename);
-
- // Reject symlinks before canonicalizing — prevents following malicious symlinks
- match file_path.symlink_metadata() {
- Ok(sym_meta) => {
- if sym_meta.file_type().is_symlink() {
- return (
- StatusCode::FORBIDDEN,
- Json(err_response("Access denied: symlinks are not allowed", "SYMLINK_DENIED")),
- )
- .into_response();
- }
- }
- Err(_) => {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- }
-
- // Double-check the resolved path is still inside results_dir (canonicalize to prevent path escapes)
- match file_path.canonicalize() {
- Ok(canonical) => {
- let results_canonical = results_dir.canonicalize().unwrap_or(results_dir.clone());
- if !canonical.starts_with(&results_canonical) {
- return (
- StatusCode::FORBIDDEN,
- Json(err_response("Access denied: path escapes results directory", "PATH_TRAVERSAL")),
- )
- .into_response();
- }
- }
- Err(_) => {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- }
-
- // Verify it's a regular file (not a directory, device, etc.)
- match file_path.metadata() {
- Ok(meta) if !meta.is_file() => {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Requested path is not a regular file", "NOT_A_FILE")),
- )
- .into_response();
- }
- Err(_) => {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- _ => {}
- }
-
- match std::fs::read_to_string(&file_path) {
- Ok(content) => {
- // Cap at 1MB to prevent memory exhaustion
- let max_size = 1024 * 1024;
- let content = if content.len() > max_size {
- format!("{}\n... (truncated at {} bytes, total: {} bytes)",
- &content[..max_size], max_size, content.len())
- } else {
- content
- };
-
- (
- StatusCode::OK,
- Json(ok_response(
- format!("File '{}' retrieved", filename),
- Some(serde_json::json!({
- "filename": filename,
- "content": content,
- "size_bytes": file_path.metadata().map(|m| m.len()).unwrap_or(0),
- })),
- )),
- )
- .into_response()
- }
- Err(e) => (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Could not read file '{}': {}", filename, e),
- "READ_ERROR",
- )),
- )
- .into_response(),
- }
-}
-
-/// POST /api/honeypot-check — honeypot port scan (like CLI pre-run check)
-async fn honeypot_check(Json(payload): Json) -> Response {
- let target_raw = payload.target.as_str();
-
- if !validate_target(target_raw) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid target format", "INVALID_TARGET")),
- )
- .into_response();
- }
-
-
- let ip = match crate::utils::extract_ip_from_target(target_raw) {
- Some(ip) => ip,
- None => {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Could not extract IP from target",
- "INVALID_TARGET",
- )),
- )
- .into_response();
- }
- };
-
- const COMMON_PORTS: &[u16] = &[
- 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389,
- 5900, 8080, 8443, 8888, 9090, 1433, 1521, 5432, 6379, 11211, 27017, 161, 389, 636, 902,
- 1080, 1194, 1883, 5672, 8883, 9200, 15672, 25565, 27018, 28017, 50000, 50070, 61616,
- ];
-
- let scan_timeout = std::time::Duration::from_millis(500);
- let semaphore = Arc::new(tokio::sync::Semaphore::new(50));
- let mut tasks = Vec::new();
-
- for &port in COMMON_PORTS {
- let ip = ip.clone();
- let sem = semaphore.clone();
- tasks.push(tokio::spawn(async move {
- let _permit = match sem.acquire().await {
- Ok(permit) => permit,
- Err(e) => {
- eprintln!("[!] Semaphore acquire failed: {}", e);
- return None;
- }
- };
- let addr = format!("{}:{}", ip, port);
- let conn = tokio::time::timeout(scan_timeout, tokio::net::TcpStream::connect(&addr))
- .await;
- if let Ok(Ok(_)) = conn {
- Some(port)
- } else {
- None
- }
- }));
- }
-
- let mut open_ports: Vec = Vec::new();
- for task in tasks {
- if let Ok(Some(port)) = task.await {
- open_ports.push(port);
- }
- }
- open_ports.sort();
-
- let is_honeypot = open_ports.len() >= 11;
-
- (
- StatusCode::OK,
- Json(ok_response(
- format!("Honeypot check completed for {}", ip),
- Some(serde_json::json!({
- "target": ip,
- "open_ports": open_ports,
- "open_count": open_ports.len(),
- "total_scanned": COMMON_PORTS.len(),
- "is_honeypot": is_honeypot,
- "threshold": 11,
- })),
- )),
- )
- .into_response()
-}
-
-// ─── Shell-Parity Exec Endpoint ─────────────────────────────────────
-
-/// POST /api/shell — run interactive-shell commands remotely (mirrors the interactive `rsf>` shell)
-///
-/// Supports all interactive shell commands: `use`, `set target`, `set subnet`, `show_target`,
-/// `clear_target`, `run`, `run_all`, `find`, `modules`, `back`, `info`, `check`, `setg`, `unsetg`,
-/// `show_options`, `creds`, `hosts`, `services`, `notes`, `workspace`, `loot`, `export`, `jobs`, `spool`.
-///
-/// For direct module execution with prompts, prefer POST /api/run instead.
-/// Supports secure command chaining via JSON array `commands` field.
-/// Each command is individually validated — no shell metacharacters allowed.
-/// Interactive-prompt commands (creds add, services add, loot add) accept inline arguments.
-async fn shell_command(
- State(state): State,
- Json(payload): Json,
-) -> Response {
- // Build command list from either `commands` (array) or `command` (single string)
- let command_list: Vec = if let Some(cmds) = &payload.commands {
- if cmds.is_empty() || cmds.len() > 20 {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Commands array must have 1-20 entries", "INVALID_INPUT")),
- ).into_response();
- }
- cmds.clone()
- } else if let Some(cmd) = &payload.command {
- vec![cmd.clone()]
- } else {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Provide 'command' (string) or 'commands' (array)", "INVALID_INPUT")),
- ).into_response();
- };
-
- let mut results: Vec = Vec::new();
-
- for raw_cmd in &command_list {
- let trimmed = raw_cmd.trim().to_string();
- if trimmed.is_empty() || trimmed.len() > 4096 {
- results.push(ShellResult {
- command: trimmed,
- success: false,
- output: "Command is empty or too long (max 4096 chars)".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- // Validate each individual command against shell metacharacters
- if contains_shell_metacharacters(&trimmed) {
- results.push(ShellResult {
- command: trimmed,
- success: false,
- output: "Command contains forbidden characters (& | ; ` $ > <). Use the 'commands' JSON array for chaining.".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- let mut parts = trimmed.splitn(2, char::is_whitespace);
- let cmd = match parts.next() {
- Some(c) => c.to_lowercase(),
- None => continue,
- };
- let rest = parts.next().unwrap_or("").trim().to_string();
- let command_key = crate::shell::resolve_command(&cmd);
-
- let start = std::time::Instant::now();
-
- match command_key.as_str() {
- "help" => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Available commands (same as interactive shell):\n\
- \n\
- ── Navigation & Discovery ──\n\
- help | h | ? — This help\n\
- modules | ls | m — List all modules\n\
- find | f1 — Search modules by keyword\n\
- use | u — Select a module\n\
- info [path] | i — Show module metadata\n\
- back | b — Deselect current module\n\
- \n\
- ── Targeting ──\n\
- set target | t — Set target (single IP/hostname)\n\
- set subnet | sn — Set target to CIDR subnet\n\
- set port — Set global port\n\
- show_target | st — Show current target & module\n\
- clear_target | ct — Clear target\n\
- \n\
- ── Execution ──\n\
- run [target] — Run selected module\n\
- run -j — Run module as background job\n\
- run_all [target] — Run all modules against target\n\
- check | ch — Non-destructive vulnerability check\n\
- \n\
- ── Global Options ──\n\
- setg | sg — Set global option\n\
- unsetg | ug — Unset global option\n\
- show_options | so — Display all global options\n\
- \n\
- ── Data Management ──\n\
- creds — List credentials\n\
- creds add [type] — Add credential\n\
- creds search — Search credentials\n\
- creds delete — Delete credential\n\
- creds clear — Clear all credentials\n\
- hosts — List tracked hosts\n\
- hosts add — Add host\n\
- hosts delete — Remove host and its services\n\
- hosts clear — Clear all hosts and services\n\
- services — List tracked services\n\
- services add [ver] — Add service\n\
- services delete — Remove a service\n\
- notes — Add note to host\n\
- workspace [name] | ws — List or switch workspaces\n\
- loot — List loot\n\
- loot add — Add loot\n\
- loot search — Search loot\n\
- loot delete — Delete loot entry\n\
- loot clear — Clear all loot\n\
- \n\
- ── Automation & Export ──\n\
- export — Export engagement data\n\
- spool [off|file] — Control output logging\n\
- jobs | j — List background jobs\n\
- jobs -k — Kill a background job\n\
- jobs clean — Clean up finished jobs\n\
- exit — (no-op in API mode)"
- .to_string(),
- duration_ms: None,
- });
- }
-
- "modules" => {
- let modules = commands::discover_modules();
- let mut by_category: std::collections::BTreeMap<&str, Vec<&str>> = std::collections::BTreeMap::new();
- for m in &modules {
- let cat = m.split('/').next().unwrap_or("other");
- by_category.entry(cat).or_default().push(m.as_str());
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "total": modules.len(),
- "categories": commands::categories(),
- "modules": by_category,
- }).to_string(),
- duration_ms: None,
- });
- }
-
- "find" => {
- if rest.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: find ".to_string(),
- duration_ms: None,
- });
- } else {
- let query = rest.to_lowercase();
- let modules = commands::discover_modules();
- let matches: Vec<&String> = modules.iter()
- .filter(|m| m.to_lowercase().contains(&query))
- .collect();
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "query": query,
- "matches": matches,
- "count": matches.len(),
- }).to_string(),
- duration_ms: None,
- });
- }
- }
-
- "use" => {
- if rest.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: use ".to_string(),
- duration_ms: None,
- });
- } else {
- let sanitized = crate::shell::sanitize_module_path(&rest);
- match sanitized {
- Some(path) => {
- let modules = commands::discover_modules();
- let found = modules.iter().any(|m| m == &path)
- || modules.iter().any(|m| {
- m.rsplit_once('/').map(|(_, s)| s == path).unwrap_or(false)
- });
- if found {
- if let Ok(mut cm) = state.current_module.lock() {
- *cm = Some(path.clone());
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Module selected: {}", path),
- duration_ms: None,
- });
- } else {
- // Fuzzy suggestion
- let best = modules.iter()
- .map(|m| (m, strsim::levenshtein(&path, m)))
- .min_by_key(|(_, d)| *d);
- let suggestion = if let Some((s, d)) = best {
- if d < 5 { format!(" Did you mean: {}?", s) } else { String::new() }
- } else {
- String::new()
- };
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Unknown module '{}'.{}", path, suggestion),
- duration_ms: None,
- });
- }
- }
- None => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid module path".to_string(),
- duration_ms: None,
- });
- }
- }
- }
- }
-
- "set" => {
- // Handle "set port " and "set source_port " as global option shortcuts
- if let Some(val) = rest.strip_prefix("port ") {
- let val = val.trim();
- match val.parse::() {
- Ok(p) if p > 0 => {
- crate::global_options::GLOBAL_OPTIONS.set("port", val).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Global port set to: {}", val),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid port. Must be 1-65535.".to_string(),
- duration_ms: None,
- });
- }
- }
- } else if let Some(val) = rest.strip_prefix("source_port ") {
- let val = val.trim();
- if val == "0" || val.is_empty() {
- crate::global_options::GLOBAL_OPTIONS.unset("source_port").await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Source port cleared (will use OS-assigned).".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- match val.parse::() {
- Ok(p) if p > 0 => {
- crate::global_options::GLOBAL_OPTIONS.set("source_port", val).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Global source port set to: {}", val),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid source port. Must be 1-65535 (or 0 to clear).".to_string(),
- duration_ms: None,
- });
- }
- }
- }
- } else {
- // Mirror shell.rs: `set target `, `t `, `set subnet `, `sn `
- // Peel off leading "target ", "t ", "subnet ", "sn " keywords to extract the value
- let raw_value = if rest.starts_with("target ") {
- rest.strip_prefix("target ").unwrap_or(&rest).trim()
- } else if rest.starts_with("t ") {
- rest.strip_prefix("t ").unwrap_or(&rest).trim()
- } else {
- rest.trim()
- };
-
- if raw_value.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: set target or set subnet or t or sn ".to_string(),
- duration_ms: None,
- });
- } else if !validate_target(raw_value) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid target".to_string(),
- duration_ms: None,
- });
- } else {
- // Strip CIDR prefix if user did `set target 1.2.3.4/24` (single-IP intent)
- let ip_only = if raw_value.contains('/') {
- raw_value.split('/').next().unwrap_or(raw_value)
- } else {
- raw_value
- };
- match crate::config::GLOBAL_CONFIG.set_target(ip_only) {
- Ok(_) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Target set to: {}", ip_only),
- duration_ms: None,
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Failed to set target: {}", e),
- duration_ms: None,
- });
- }
- }
- }
- } // close else from port/source_port handling
- }
-
- "set_subnet" => {
- // Mirror shell.rs set_subnet: accepts `sn `, `subnet `, `set subnet `
- let raw_value = if rest.starts_with("subnet ") {
- rest.strip_prefix("subnet ").unwrap_or(&rest).trim()
- } else if rest.starts_with("sn ") {
- rest.strip_prefix("sn ").unwrap_or(&rest).trim()
- } else {
- rest.trim()
- };
-
- if raw_value.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: sn or set subnet (e.g. 192.168.1.0/24)".to_string(),
- duration_ms: None,
- });
- } else if !raw_value.contains('/') {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Not a subnet — use CIDR notation (e.g. 192.168.1.0/24). For single IPs use: set target ".to_string(),
- duration_ms: None,
- });
- } else if !validate_target(raw_value) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid CIDR target".to_string(),
- duration_ms: None,
- });
- } else if raw_value.parse::().is_err() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Invalid CIDR notation: {}", raw_value),
- duration_ms: None,
- });
- } else {
- match crate::config::GLOBAL_CONFIG.set_target(raw_value) {
- Ok(_) => {
- let size = crate::config::GLOBAL_CONFIG.get_target_size();
- let msg = match size {
- Some(s) => format!("Subnet set to: {} ({} IPs)", raw_value, s),
- None => format!("Subnet set to: {}", raw_value),
- };
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: msg,
- duration_ms: None,
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Failed to set subnet: {}", e),
- duration_ms: None,
- });
- }
- }
- }
- }
-
- "show_target" => {
- let target = crate::config::GLOBAL_CONFIG.get_target().unwrap_or_else(|| "Not set".to_string());
- let module = state.current_module.lock().ok().and_then(|cm| cm.clone()).unwrap_or_else(|| "None".to_string());
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "target": target,
- "current_module": module,
- }).to_string(),
- duration_ms: None,
- });
- }
-
- "clear_target" => {
- crate::config::GLOBAL_CONFIG.clear_target();
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Target cleared".to_string(),
- duration_ms: None,
- });
- }
-
- "back" => {
- if let Ok(mut cm) = state.current_module.lock() {
- *cm = None;
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Module deselected".to_string(),
- duration_ms: None,
- });
- }
-
- "run" => {
- let background = rest.trim() == "-j" || rest.trim() == "--job";
- let rest_for_target = if background { String::new() } else { rest.clone() };
-
- let module_path = state.current_module.lock().ok().and_then(|cm| cm.clone());
- if let Some(module_path) = module_path {
- // Resolve target: from rest arg, or global config
- let target = if !rest_for_target.is_empty() {
- rest_for_target.clone()
- } else if crate::config::GLOBAL_CONFIG.has_target() {
- crate::config::GLOBAL_CONFIG.get_target().unwrap_or_default()
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No target set. Use 'set ' first.".to_string(),
- duration_ms: None,
- });
- continue;
- };
-
- // SSRF guard — shell 'run' must also validate the resolved target
- if !validate_target(&target) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Target is invalid or blocked (internal/private address)".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- let verbose = state.verbose;
-
- if background {
- // Background job: spawn via JOB_MANAGER
- let job_id = crate::jobs::JOB_MANAGER.spawn(
- module_path.clone(), target.clone(), verbose,
- );
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Job {} started: {} against {}", job_id, module_path, target),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- // Foreground execution
- let run_start = std::time::Instant::now();
- let _permit = match state.run_semaphore.acquire().await {
- Ok(permit) => permit,
- Err(_) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Server is shutting down".to_string(),
- duration_ms: None,
- });
- continue;
- }
- };
- match commands::run_module(&module_path, &target, verbose).await {
- Ok(_) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Module '{}' executed against '{}'", module_path, target),
- duration_ms: Some(run_start.elapsed().as_millis() as u64),
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Module failed: {}", e),
- duration_ms: Some(run_start.elapsed().as_millis() as u64),
- });
- }
- }
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No module selected. Use 'use ' first.".to_string(),
- duration_ms: None,
- });
- }
- }
-
- "run_all" => {
- let target = if !rest.is_empty() {
- rest.clone()
- } else if crate::config::GLOBAL_CONFIG.has_target() {
- crate::config::GLOBAL_CONFIG.get_target().unwrap_or_default()
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No target set for run_all".to_string(),
- duration_ms: None,
- });
- continue;
- };
-
- // SSRF guard — run_all must check the resolved target too
- if !validate_target(&target) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Target is invalid or blocked (internal/private address)".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- let modules = commands::discover_modules();
- let verbose = state.verbose;
- let run_start = std::time::Instant::now();
- let mut ok_count = 0usize;
- let mut fail_count = 0usize;
- for m in &modules {
- let _permit = match state.run_semaphore.acquire().await {
- Ok(permit) => permit,
- Err(_) => {
- fail_count += 1;
- continue;
- }
- };
- match commands::run_module(m, &target, verbose).await {
- Ok(_) => ok_count += 1,
- Err(_) => fail_count += 1,
- }
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("run_all complete: {} ok, {} failed out of {} modules",
- ok_count, fail_count, modules.len()),
- duration_ms: Some(run_start.elapsed().as_millis() as u64),
- });
- }
-
- // ═══════════════════════════════════════════════
- // INFO — Module metadata
- // ═══════════════════════════════════════════════
- "info" => {
- let module_path = if !rest.is_empty() {
- Some(rest.clone())
- } else {
- state.current_module.lock().ok().and_then(|cm| cm.clone())
- };
- if let Some(ref path) = module_path {
- if let Some(info) = commands::module_info(path) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "module": path,
- "info": info,
- }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("No metadata available for '{}'. Modules can provide metadata by adding a pub fn info() -> ModuleInfo function.", path),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No module selected. Use 'info ' or select a module first.".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // CHECK — Non-destructive vulnerability check
- // ═══════════════════════════════════════════════
- "check" => {
- let module_path = state.current_module.lock().ok().and_then(|cm| cm.clone());
- if let Some(ref path) = module_path {
- let target = crate::config::GLOBAL_CONFIG.get_target();
- if let Some(ref t) = target {
- if !validate_target(t) || is_blocked_target(t) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Target is invalid or blocked (internal/private address)".to_string(),
- duration_ms: None,
- });
- } else {
- match commands::check_module(path, t).await {
- Some(result) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "module": path,
- "target": t,
- "result": result,
- }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- None => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Module '{}' does not support the check method.", path),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No target set. Use 'set target ' first.".to_string(),
- duration_ms: None,
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No module selected. Use 'use ' first.".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // GLOBAL OPTIONS
- // ═══════════════════════════════════════════════
- "setg" => {
- if let Some((key, value)) = rest.split_once(char::is_whitespace) {
- let key = key.trim();
- let value = value.trim();
- if key.is_empty() || value.is_empty() || key.len() > 256 || value.len() > 256 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: setg (max 256 chars each)".to_string(),
- duration_ms: None,
- });
- } else {
- crate::global_options::GLOBAL_OPTIONS.set(key, value).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("{} => {}", key, value),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: setg ".to_string(),
- duration_ms: None,
- });
- }
- }
-
- "unsetg" => {
- let key = rest.trim();
- if key.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: unsetg ".to_string(),
- duration_ms: None,
- });
- } else if crate::global_options::GLOBAL_OPTIONS.unset(key).await {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Unset global option '{}'", key),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Global option '{}' was not set.", key),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
-
- "show_options" => {
- let opts = crate::global_options::GLOBAL_OPTIONS.all().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "options": opts }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
-
- // ═══════════════════════════════════════════════
- // CREDENTIALS
- // ═══════════════════════════════════════════════
- "creds" => {
- if rest.is_empty() {
- let entries = crate::cred_store::CRED_STORE.list().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "credentials": entries }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(args) = rest.strip_prefix("add ") {
- // Parse: creds add [type]
- let parts: Vec<&str> = args.splitn(6, char::is_whitespace).collect();
- if parts.len() < 5 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: creds add [type]".to_string(),
- duration_ms: None,
- });
- } else {
- let host = parts[0].trim();
- let port: u16 = match parts[1].trim().parse() {
- Ok(p) if p > 0 => p,
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid port number. Must be 1-65535.".to_string(),
- duration_ms: None,
- });
- continue;
- }
- };
- let service = parts[2].trim();
- let username = parts[3].trim();
- let (secret, cred_type) = if parts.len() >= 6 {
- (parts[4].trim(), match parts[5].trim() {
- "hash" => crate::cred_store::CredType::Hash,
- "key" => crate::cred_store::CredType::Key,
- "token" => crate::cred_store::CredType::Token,
- _ => crate::cred_store::CredType::Password,
- })
- } else {
- (parts[4].trim(), crate::cred_store::CredType::Password)
- };
- let id = crate::cred_store::CRED_STORE.add(host, port, service, username, secret, cred_type, "api-shell").await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Credential stored (ID: {})", id),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else if let Some(query) = rest.strip_prefix("search ") {
- let found = crate::cred_store::CRED_STORE.search(query.trim()).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "results": found }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(id) = rest.strip_prefix("delete ") {
- let deleted = crate::cred_store::CRED_STORE.delete(id.trim()).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: deleted,
- output: if deleted {
- format!("Credential '{}' deleted.", id.trim())
- } else {
- format!("Credential '{}' not found.", id.trim())
- },
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if rest == "clear" {
- crate::cred_store::CRED_STORE.clear().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "All credentials cleared.".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if rest == "add" {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: creds add [type]".to_string(),
- duration_ms: None,
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: creds [add [type]|search |delete |clear]".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // SPOOL — Output logging
- // ═══════════════════════════════════════════════
- "spool" => {
- if rest.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Use 'spool ' to start or 'spool off' to stop.".to_string(),
- duration_ms: None,
- });
- } else if rest == "off" {
- if let Some(name) = crate::spool::SPOOL.stop() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Spool stopped. Output saved to '{}'", name),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Spool was not active.".to_string(),
- duration_ms: None,
- });
- }
- } else {
- match crate::spool::SPOOL.start(&rest) {
- Ok(()) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Spooling output to '{}'", rest),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Failed to start spool: {}", e),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
- }
- }
-
- // ═══════════════════════════════════════════════
- // RESOURCE / MAKERC — Blocked in API mode
- // ═══════════════════════════════════════════════
- "resource" => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Resource scripts are disabled in API mode for security. Send commands directly via the 'commands' JSON array.".to_string(),
- duration_ms: None,
- });
- }
-
- "makerc" => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "makerc is not applicable in API mode (no shell history).".to_string(),
- duration_ms: None,
- });
- }
-
- // ═══════════════════════════════════════════════
- // HOSTS
- // ═══════════════════════════════════════════════
- "hosts" => {
- if rest.is_empty() {
- let entries = crate::workspace::WORKSPACE.hosts().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "hosts": entries }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(ip) = rest.strip_prefix("add ") {
- let ip = ip.trim();
- if ip.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: hosts add ".to_string(),
- duration_ms: None,
- });
- } else {
- crate::workspace::WORKSPACE.add_host(ip, None, None).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Host '{}' added to workspace.", ip),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else if let Some(ip) = rest.strip_prefix("delete ") {
- let ip = ip.trim();
- if ip.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: hosts delete ".to_string(),
- duration_ms: None,
- });
- } else {
- let deleted = crate::workspace::WORKSPACE.delete_host(ip).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: deleted,
- output: if deleted {
- format!("Host '{}' and its services removed.", ip)
- } else {
- format!("Host '{}' not found.", ip)
- },
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else if rest == "clear" {
- crate::workspace::WORKSPACE.clear_hosts().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "All hosts and services cleared.".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: hosts [add |delete |clear]".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // SERVICES
- // ═══════════════════════════════════════════════
- "services" => {
- if rest.is_empty() {
- let entries = crate::workspace::WORKSPACE.services().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "services": entries }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(args) = rest.strip_prefix("add ") {
- // Parse: services add [version]
- let parts: Vec<&str> = args.splitn(5, char::is_whitespace).collect();
- if parts.len() < 4 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: services add [version]".to_string(),
- duration_ms: None,
- });
- } else {
- let host = parts[0].trim();
- let port: u16 = match parts[1].trim().parse() {
- Ok(p) if p > 0 => p,
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid port number. Must be 1-65535.".to_string(),
- duration_ms: None,
- });
- continue;
- }
- };
- let proto = parts[2].trim();
- let svc = parts[3].trim();
- let version = if parts.len() >= 5 { Some(parts[4].trim()) } else { None };
- crate::workspace::WORKSPACE.add_service(host, port, proto, svc, version).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Service {}:{}/{} added.", host, port, svc),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else if rest == "add" {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: services add [version]".to_string(),
- duration_ms: None,
- });
- } else if let Some(args) = rest.strip_prefix("delete ") {
- let parts: Vec<&str> = args.splitn(2, char::is_whitespace).collect();
- if parts.len() < 2 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: services delete ".to_string(),
- duration_ms: None,
- });
- } else {
- let host = parts[0].trim();
- match parts[1].trim().parse::() {
- Ok(port) if port > 0 => {
- let deleted = crate::workspace::WORKSPACE.delete_service(host, port).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: deleted,
- output: if deleted {
- format!("Service {}:{} removed.", host, port)
- } else {
- format!("Service {}:{} not found.", host, port)
- },
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid port number. Must be 1-65535.".to_string(),
- duration_ms: None,
- });
- }
- }
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: services [add [version]|delete ]".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // NOTES
- // ═══════════════════════════════════════════════
- "notes" => {
- if let Some((ip, note)) = rest.split_once(char::is_whitespace) {
- let ip = ip.trim();
- let note = note.trim();
- if ip.is_empty() || note.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: notes ".to_string(),
- duration_ms: None,
- });
- } else if crate::workspace::WORKSPACE.add_note(ip, note).await {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Note added to host '{}'.", ip),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Host '{}' not found. Add it first with 'hosts add {}'.", ip, ip),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: notes ".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // WORKSPACE
- // ═══════════════════════════════════════════════
- "workspace" => {
- if rest.is_empty() {
- let current = crate::workspace::WORKSPACE.current_name().await;
- let workspaces = crate::workspace::WORKSPACE.list_workspaces().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "current": current,
- "workspaces": workspaces,
- }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- let name = rest.trim();
- if name.is_empty() || name.len() > 64 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Workspace name must be 1-64 characters.".to_string(),
- duration_ms: None,
- });
- } else if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
- crate::workspace::WORKSPACE.switch(name).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Switched to workspace '{}'", name),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Workspace name must be alphanumeric (with _ and -).".to_string(),
- duration_ms: None,
- });
- }
- }
- }
-
- // ═══════════════════════════════════════════════
- // LOOT
- // ═══════════════════════════════════════════════
- "loot" => {
- if rest.is_empty() {
- let entries = crate::loot::LOOT_STORE.list().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "loot": entries }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(args) = rest.strip_prefix("add ") {
- // Parse: loot add
- let parts: Vec<&str> = args.splitn(4, char::is_whitespace).collect();
- if parts.len() < 4 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: loot add ".to_string(),
- duration_ms: None,
- });
- } else {
- let host = parts[0].trim();
- let ltype = parts[1].trim();
- let desc = parts[2].trim();
- let data = parts[3].trim();
- if let Some(id) = crate::loot::LOOT_STORE.add_text(host, ltype, desc, data, "api-shell").await {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Loot stored (ID: {})", id),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Failed to store loot.".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
- } else if let Some(query) = rest.strip_prefix("search ") {
- let found = crate::loot::LOOT_STORE.search(query.trim()).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "results": found }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if rest == "add" {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: loot add ".to_string(),
- duration_ms: None,
- });
- } else if let Some(id) = rest.strip_prefix("delete ") {
- let id = id.trim();
- if id.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: loot delete ".to_string(),
- duration_ms: None,
- });
- } else {
- let deleted = crate::loot::LOOT_STORE.delete(id).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: deleted,
- output: if deleted {
- format!("Loot '{}' deleted.", id)
- } else {
- format!("Loot '{}' not found.", id)
- },
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else if rest == "clear" {
- crate::loot::LOOT_STORE.clear().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "All loot cleared.".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: loot [add |search |delete |clear]".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // EXPORT
- // ═══════════════════════════════════════════════
- "export" => {
- if let Some((fmt, path)) = rest.split_once(char::is_whitespace) {
- let path = path.trim();
- if path.is_empty() || path.contains("..") || path.contains('\0') || path.contains('/') || path.contains('\\') {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid file path.".to_string(),
- duration_ms: None,
- });
- } else {
- let export_result = match fmt.trim() {
- "json" => crate::export::export_json(path).await,
- "csv" => crate::export::export_csv(path).await,
- "summary" => crate::export::export_summary(path).await,
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: export ".to_string(),
- duration_ms: None,
- });
- continue;
- }
- };
- match export_result {
- Ok(()) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Exported {} to '{}'", fmt.trim(), path),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Export failed: {}", e),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: export