CLI based Pentesting Tool
  • Rust 98%
  • Nix 2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-30 17:15:21 +02:00
.github/workflows Create rust.yml 2026-06-25 20:33:56 +02:00
src Add user input module support 2026-06-25 16:19:14 +02:00
.envrc add flake config 2026-07-18 23:35:02 +02:00
.gitignore add build and run nix config 2026-07-21 15:36:29 +02:00
Cargo.lock Initial Based Structure 2026-06-19 01:07:31 +02:00
Cargo.toml Initial Based Structure 2026-06-19 01:07:31 +02:00
flake.lock Update flake 2026-07-30 17:15:21 +02:00
flake.nix Add darwin to flake 2026-07-24 17:12:21 +02:00
README.md Update README.md 2026-06-25 20:35:39 +02:00

PenSuite

Rust

A modular terminal UI for penetration testing, built with Ratatui.

Modules are plain Rust files - implement one trait, register one line, recompile.

Screenshot 2026-06-25 at 16 39 58

Features

  • TUI with keyboard-driven navigation and a calm, low-contrast colour scheme
  • Auto-detects target type (URL, IP, Hostname) and shows only compatible modules
  • Each module lists its supported target types as tags in the overview
  • Fuzzy search the module list — press / and type
  • Modules can request user input — collected in a floating form before the run
  • Modules run on a background thread with an animated loading screen (no UI freeze)
  • Pass a target directly as a CLI argument to skip the prompt
  • Double Ctrl-C to quit from anywhere
  • Scrollable module output

Installation

git clone <repo>
cd pensuite
cargo build --release
# binary is at target/release/pensuite

Requires Rust 1.85+ (edition 2024).


Usage

# Interactive — prompts for a target on launch
pensuite

# Skip the prompt — target is classified and the dashboard opens immediately
pensuite https://example.com
pensuite 192.168.1.1
pensuite internal.corp

Keybindings

Target input

Key Action
Enter Confirm target (non-empty required)
Esc Cancel and return to dashboard (only if a target is already set)
Backspace Delete last character

Dashboard

Key Action
t Edit current target
/ Search / filter the module list
/ k Move selection up
/ j Move selection down
Enter Run selected module (or open its input form)
q Quit
Ctrl-C × 2 Quit (works on every screen)

Search (after pressing /)

Key Action
(type) Filter modules by name / description
/ Move selection within the filtered list
Enter Run selected module
Backspace Delete last character
Esc Clear search and exit search mode

Module input form

Shown automatically when a module declares inputs.

Key Action
(type) Edit the focused field
/ / Tab Move between fields
Enter Next field, or run when on the last field
Esc Cancel and return to dashboard

Running

Key Action
Esc Stop waiting and return to dashboard (the worker is detached)

Module output

Key Action
q / Esc Back to dashboard
/ k Scroll up
/ j Scroll down

Target detection

When a target is entered the type is detected automatically and displayed as a badge in the header. Only modules that declare support for that type are shown.

Detected as Example inputs
URL https://example.com, http://10.0.0.1:8080/path
IP 192.168.1.1, 10.0.0.1:8080, ::1, [::1]:9090
Hostname example.com, localhost, internal.corp
(unknown) Anything that doesn't match — all modules remain visible

Built-in modules

Name Supported targets Description
http-get URL GET request — shows status, headers, and body
port-scan IP, Hostname TCP connect scan across a configurable list of ports (asks for ports and timeout_ms)
react-ssr-rce URL CVE-2025-55182 React2Shell check (asks for the route to probe)

Modules that declare inputs prompt for them in a floating form before running; the collected values are passed to the module alongside the target.


Adding a module

1. Create the filesrc/modules/my_module.rs:

use crate::target::TargetKind;
use super::{InputSpec, Module, RunContext};

pub struct MyModule;

impl Module for MyModule {
    fn name(&self) -> &str { "my-module" }

    fn description(&self) -> &str { "Does something useful" }

    // Omit supported_targets() to accept any recognised target type,
    // or return a slice to restrict:
    fn supported_targets(&self) -> &[TargetKind] {
        &[TargetKind::Ip, TargetKind::Hostname]
    }

    // Optional — omit for a module that needs no input. Declared inputs are
    // collected in a floating form before run() is called.
    fn inputs(&self) -> Vec<InputSpec> {
        vec![InputSpec::new("port", "Port to probe", "8080")]
    }

    // run() executes on a background thread. `ctx` carries the target plus any
    // collected inputs; reach them with ctx.target and ctx.input("key").
    fn run(&self, ctx: &RunContext) -> Result<String, String> {
        Ok(format!("ran against {} on port {}", ctx.target, ctx.input("port")))
    }
}

2. Expose it — add one line to src/modules/mod.rs:

pub mod my_module;

3. Register it — add one line to main() in src/main.rs:

app.modules.register(Arc::new(modules::my_module::MyModule));

That's it. Recompile and the module appears in the list for compatible targets.

Modules run on a worker thread, so they must be Send + Sync (the trait requires it) and are registered as Arc<dyn Module>.


Project structure

src/
├── main.rs              — entry point, module registration
├── app.rs               — state machine, event handling, background runner
├── ui.rs                — all rendering (Ratatui)
├── target.rs            — TargetKind enum and auto-detection
└── modules/
    ├── mod.rs           — Module trait, InputSpec, RunContext, registry
    ├── http_get.rs      — built-in HTTP GET module
    ├── port_scan.rs     — TCP port scanner (example of user inputs)
    └── react_ssr_rce.rs — CVE-2025-55182 React2Shell check