mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-03-15 14:05:59 +00:00
Compare commits
2 Commits
refactor/r
...
feature/tu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9376645197 | ||
|
|
3e0d1cd02f |
413
USAGE.md
413
USAGE.md
@@ -1,8 +1,9 @@
|
|||||||
# FuzzForge AI Usage Guide
|
# FuzzForge AI Usage Guide
|
||||||
|
|
||||||
This guide covers everything you need to know to get started with FuzzForge AI - from installation to running your first security research workflow with AI.
|
This guide covers everything you need to know to get started with FuzzForge AI — from installation to linking your first MCP hub and running security research workflows with AI.
|
||||||
|
|
||||||
> **FuzzForge is designed to be used with AI agents** (GitHub Copilot, Claude, etc.) via MCP.
|
> **FuzzForge is designed to be used with AI agents** (GitHub Copilot, Claude, etc.) via MCP.
|
||||||
|
> A terminal UI (`fuzzforge ui`) is provided for managing agents and hubs.
|
||||||
> The CLI is available for advanced users but the primary experience is through natural language interaction with your AI assistant.
|
> The CLI is available for advanced users but the primary experience is through natural language interaction with your AI assistant.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -12,8 +13,17 @@ This guide covers everything you need to know to get started with FuzzForge AI -
|
|||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Building Modules](#building-modules)
|
- [Terminal UI](#terminal-ui)
|
||||||
- [MCP Server Configuration](#mcp-server-configuration)
|
- [Launching the UI](#launching-the-ui)
|
||||||
|
- [Dashboard](#dashboard)
|
||||||
|
- [Agent Setup](#agent-setup)
|
||||||
|
- [Hub Manager](#hub-manager)
|
||||||
|
- [MCP Hub System](#mcp-hub-system)
|
||||||
|
- [What is an MCP Hub?](#what-is-an-mcp-hub)
|
||||||
|
- [FuzzingLabs Security Hub](#fuzzinglabs-security-hub)
|
||||||
|
- [Linking a Custom Hub](#linking-a-custom-hub)
|
||||||
|
- [Building Hub Images](#building-hub-images)
|
||||||
|
- [MCP Server Configuration (CLI)](#mcp-server-configuration-cli)
|
||||||
- [GitHub Copilot](#github-copilot)
|
- [GitHub Copilot](#github-copilot)
|
||||||
- [Claude Code (CLI)](#claude-code-cli)
|
- [Claude Code (CLI)](#claude-code-cli)
|
||||||
- [Claude Desktop](#claude-desktop)
|
- [Claude Desktop](#claude-desktop)
|
||||||
@@ -27,7 +37,7 @@ This guide covers everything you need to know to get started with FuzzForge AI -
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
> **Prerequisites:** You need [uv](https://docs.astral.sh/uv/) and [Docker](https://docs.docker.com/get-docker/) installed.
|
> **Prerequisites:** You need [uv](https://docs.astral.sh/uv/) and [Docker](https://docs.docker.com/get-docker/) installed.
|
||||||
> See the [Prerequisites](#prerequisites) section for installation instructions.
|
> See the [Prerequisites](#prerequisites) section for details.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone and install
|
# 1. Clone and install
|
||||||
@@ -35,20 +45,29 @@ git clone https://github.com/FuzzingLabs/fuzzforge_ai.git
|
|||||||
cd fuzzforge_ai
|
cd fuzzforge_ai
|
||||||
uv sync
|
uv sync
|
||||||
|
|
||||||
# 2. Build the module images (one-time setup)
|
# 2. Launch the terminal UI
|
||||||
make build-modules
|
uv run fuzzforge ui
|
||||||
|
|
||||||
# 3. Install MCP for your AI agent
|
# 3. Press 'h' → "FuzzingLabs Hub" to clone & link the default security hub
|
||||||
uv run fuzzforge mcp install copilot # For VS Code + GitHub Copilot
|
# 4. Select an agent row and press Enter to link it
|
||||||
# OR
|
# 5. Restart your AI agent and start talking:
|
||||||
uv run fuzzforge mcp install claude-code # For Claude Code CLI
|
# "What security tools are available?"
|
||||||
|
# "Scan this binary with binwalk and yara"
|
||||||
# 4. Restart your AI agent (VS Code, Claude, etc.)
|
|
||||||
|
|
||||||
# 5. Start talking to your AI:
|
|
||||||
# "List available FuzzForge modules"
|
|
||||||
# "Analyze this Rust crate for fuzzable functions"
|
# "Analyze this Rust crate for fuzzable functions"
|
||||||
# "Start fuzzing the parse_input function"
|
```
|
||||||
|
|
||||||
|
Or do it entirely from the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install MCP for your AI agent
|
||||||
|
uv run fuzzforge mcp install copilot # For VS Code + GitHub Copilot
|
||||||
|
# OR
|
||||||
|
uv run fuzzforge mcp install claude-code # For Claude Code CLI
|
||||||
|
|
||||||
|
# Build hub tool images
|
||||||
|
./scripts/build-hub-images.sh
|
||||||
|
|
||||||
|
# Restart your AI agent — done!
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** FuzzForge uses Docker by default. Podman is also supported via `--engine podman`.
|
> **Note:** FuzzForge uses Docker by default. Podman is also supported via `--engine podman`.
|
||||||
@@ -59,9 +78,10 @@ uv run fuzzforge mcp install claude-code # For Claude Code CLI
|
|||||||
|
|
||||||
Before installing FuzzForge AI, ensure you have:
|
Before installing FuzzForge AI, ensure you have:
|
||||||
|
|
||||||
- **Python 3.12+** - [Download Python](https://www.python.org/downloads/)
|
- **Python 3.12+** — [Download Python](https://www.python.org/downloads/)
|
||||||
- **uv** package manager - [Install uv](https://docs.astral.sh/uv/)
|
- **uv** package manager — [Install uv](https://docs.astral.sh/uv/)
|
||||||
- **Docker** - Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
- **Docker** — Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
||||||
|
- **Git** — For cloning hub repositories
|
||||||
|
|
||||||
### Installing uv
|
### Installing uv
|
||||||
|
|
||||||
@@ -115,74 +135,164 @@ uv run fuzzforge --help
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Building Modules
|
## Terminal UI
|
||||||
|
|
||||||
FuzzForge modules are containerized security tools. After cloning, you need to build them once:
|
FuzzForge ships with a terminal user interface (TUI) built on [Textual](https://textual.textualize.io/) for managing AI agents and MCP hub servers from a single dashboard.
|
||||||
|
|
||||||
### Build All Modules
|
### Launching the UI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From the fuzzforge_ai directory
|
uv run fuzzforge ui
|
||||||
make build-modules
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds all available modules:
|
### Dashboard
|
||||||
- `fuzzforge-rust-analyzer` - Analyzes Rust code for fuzzable functions
|
|
||||||
- `fuzzforge-cargo-fuzzer` - Runs cargo-fuzz on Rust crates
|
|
||||||
- `fuzzforge-harness-validator` - Validates generated fuzzing harnesses
|
|
||||||
- `fuzzforge-crash-analyzer` - Analyzes crash inputs
|
|
||||||
|
|
||||||
### Build a Single Module
|
The main screen is split into two panels:
|
||||||
|
|
||||||
```bash
|
| Panel | Content |
|
||||||
# Build a specific module
|
|-------|---------|
|
||||||
cd fuzzforge-modules/rust-analyzer
|
| **AI Agents** (left) | Shows GitHub Copilot, Claude Desktop, and Claude Code with live link status and config file path |
|
||||||
make build
|
| **Hub Servers** (right) | Shows all configured MCP hub tools with Docker image name, source hub, and build status (✓ Ready / ✗ Not built) |
|
||||||
```
|
|
||||||
|
|
||||||
### Verify Modules are Built
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
```bash
|
| Key | Action |
|
||||||
# List built module images
|
|-----|--------|
|
||||||
docker images | grep fuzzforge
|
| `Enter` | **Select** — Act on the selected row (setup/unlink an agent) |
|
||||||
```
|
| `h` | **Hub Manager** — Open the hub management screen |
|
||||||
|
| `r` | **Refresh** — Re-check all agent and hub statuses |
|
||||||
|
| `q` | **Quit** |
|
||||||
|
|
||||||
You should see something like:
|
### Agent Setup
|
||||||
```
|
|
||||||
fuzzforge-rust-analyzer 0.1.0 abc123def456 2 minutes ago 850 MB
|
Select an agent row in the AI Agents table and press `Enter`:
|
||||||
fuzzforge-cargo-fuzzer 0.1.0 789ghi012jkl 2 minutes ago 1.2 GB
|
|
||||||
...
|
- **If the agent is not linked** → a setup dialog opens asking for your container engine (Docker or Podman), then installs the FuzzForge MCP configuration
|
||||||
```
|
- **If the agent is already linked** → a confirmation dialog offers to unlink it (removes the `fuzzforge` entry without touching other MCP servers)
|
||||||
|
|
||||||
|
The setup auto-detects:
|
||||||
|
- FuzzForge installation root
|
||||||
|
- Docker/Podman socket path
|
||||||
|
- Hub configuration from `hub-config.json`
|
||||||
|
|
||||||
|
### Hub Manager
|
||||||
|
|
||||||
|
Press `h` to open the hub manager. This is where you manage your MCP hub repositories:
|
||||||
|
|
||||||
|
| Button | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| **FuzzingLabs Hub** | One-click clone of the official [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub) repository — clones to `~/.fuzzforge/hubs/mcp-security-hub`, scans for tools, and registers them in `hub-config.json` |
|
||||||
|
| **Link Path** | Link any local directory as a hub — enter a name and path, FuzzForge scans it for `category/tool-name/Dockerfile` patterns |
|
||||||
|
| **Clone URL** | Clone any git repository and link it as a hub |
|
||||||
|
| **Remove** | Unlink the selected hub and remove its servers from the configuration |
|
||||||
|
|
||||||
|
The hub table shows:
|
||||||
|
- **Name** — Hub name (★ prefix for the default hub)
|
||||||
|
- **Path** — Local directory path
|
||||||
|
- **Servers** — Number of MCP tools discovered
|
||||||
|
- **Source** — Git URL or "local"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MCP Server Configuration
|
## MCP Hub System
|
||||||
|
|
||||||
FuzzForge integrates with AI agents through the Model Context Protocol (MCP). Configure your preferred AI agent to use FuzzForge tools.
|
### What is an MCP Hub?
|
||||||
|
|
||||||
|
An MCP hub is a directory containing one or more containerized MCP tools, organized by category:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-hub/
|
||||||
|
├── category-a/
|
||||||
|
│ ├── tool-1/
|
||||||
|
│ │ └── Dockerfile
|
||||||
|
│ └── tool-2/
|
||||||
|
│ └── Dockerfile
|
||||||
|
├── category-b/
|
||||||
|
│ └── tool-3/
|
||||||
|
│ └── Dockerfile
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
FuzzForge scans for the pattern `category/tool-name/Dockerfile` and auto-generates server configuration entries for each discovered tool.
|
||||||
|
|
||||||
|
### FuzzingLabs Security Hub
|
||||||
|
|
||||||
|
The default MCP hub is [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub), maintained by FuzzingLabs. It includes **40+ security tools** across categories:
|
||||||
|
|
||||||
|
| Category | Tools |
|
||||||
|
|----------|-------|
|
||||||
|
| **Reconnaissance** | nmap, masscan, shodan, zoomeye, whatweb, pd-tools, externalattacker, networksdb |
|
||||||
|
| **Binary Analysis** | binwalk, yara, capa, radare2, ghidra, ida |
|
||||||
|
| **Code Security** | semgrep, rust-analyzer, harness-tester, cargo-fuzzer, crash-analyzer |
|
||||||
|
| **Web Security** | nuclei, nikto, sqlmap, ffuf, burp, waybackurls |
|
||||||
|
| **Fuzzing** | boofuzz, dharma |
|
||||||
|
| **Exploitation** | searchsploit |
|
||||||
|
| **Secrets** | gitleaks |
|
||||||
|
| **Cloud Security** | trivy, prowler, roadrecon |
|
||||||
|
| **OSINT** | maigret, dnstwist |
|
||||||
|
| **Threat Intel** | virustotal, otx |
|
||||||
|
| **Password Cracking** | hashcat |
|
||||||
|
| **Blockchain** | medusa, solazy, daml-viewer |
|
||||||
|
|
||||||
|
**Clone it via the UI:**
|
||||||
|
|
||||||
|
1. `uv run fuzzforge ui`
|
||||||
|
2. Press `h` → click **FuzzingLabs Hub**
|
||||||
|
3. Wait for the clone to finish — servers are auto-registered
|
||||||
|
|
||||||
|
**Or clone manually:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:FuzzingLabs/mcp-security-hub.git ~/.fuzzforge/hubs/mcp-security-hub
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linking a Custom Hub
|
||||||
|
|
||||||
|
You can link any directory that follows the `category/tool-name/Dockerfile` layout:
|
||||||
|
|
||||||
|
**Via the UI:**
|
||||||
|
|
||||||
|
1. Press `h` → **Link Path**
|
||||||
|
2. Enter a name and the directory path
|
||||||
|
|
||||||
|
**Via the CLI (planned):** Not yet available — use the UI.
|
||||||
|
|
||||||
|
### Building Hub Images
|
||||||
|
|
||||||
|
After linking a hub, you need to build the Docker images before the tools can be used:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build all images from the default security hub
|
||||||
|
./scripts/build-hub-images.sh
|
||||||
|
|
||||||
|
# Or build a single tool image
|
||||||
|
docker build -t semgrep-mcp:latest mcp-security-hub/code-security/semgrep-mcp/
|
||||||
|
```
|
||||||
|
|
||||||
|
The dashboard hub table shows ✓ Ready for built images and ✗ Not built for missing ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Server Configuration (CLI)
|
||||||
|
|
||||||
|
If you prefer the command line over the TUI, you can configure agents directly:
|
||||||
|
|
||||||
### GitHub Copilot
|
### GitHub Copilot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# That's it! Just run this command:
|
|
||||||
uv run fuzzforge mcp install copilot
|
uv run fuzzforge mcp install copilot
|
||||||
```
|
```
|
||||||
|
|
||||||
The command auto-detects everything:
|
The command auto-detects:
|
||||||
- **FuzzForge root** - Where FuzzForge is installed
|
- **FuzzForge root** — Where FuzzForge is installed
|
||||||
- **Modules path** - Defaults to `fuzzforge_ai/fuzzforge-modules`
|
- **Docker socket** — Auto-detects `/var/run/docker.sock`
|
||||||
- **Docker socket** - Auto-detects `/var/run/docker.sock`
|
|
||||||
|
|
||||||
**Optional overrides** (usually not needed):
|
**Optional overrides:**
|
||||||
```bash
|
```bash
|
||||||
uv run fuzzforge mcp install copilot \
|
uv run fuzzforge mcp install copilot --engine podman
|
||||||
--modules /path/to/modules \
|
|
||||||
--engine podman # if using Podman instead of Docker
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**After installation:**
|
**After installation:** Restart VS Code. FuzzForge tools appear in GitHub Copilot Chat.
|
||||||
1. Restart VS Code
|
|
||||||
2. Open GitHub Copilot Chat
|
|
||||||
3. FuzzForge tools are now available!
|
|
||||||
|
|
||||||
### Claude Code (CLI)
|
### Claude Code (CLI)
|
||||||
|
|
||||||
@@ -190,143 +300,89 @@ uv run fuzzforge mcp install copilot \
|
|||||||
uv run fuzzforge mcp install claude-code
|
uv run fuzzforge mcp install claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
Installs to `~/.claude.json` so FuzzForge tools are available from any directory.
|
Installs to `~/.claude.json`. FuzzForge tools are available from any directory after restarting Claude.
|
||||||
|
|
||||||
**After installation:**
|
|
||||||
1. Run `claude` from any directory
|
|
||||||
2. FuzzForge tools are now available!
|
|
||||||
|
|
||||||
### Claude Desktop
|
### Claude Desktop
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Automatic installation
|
|
||||||
uv run fuzzforge mcp install claude-desktop
|
uv run fuzzforge mcp install claude-desktop
|
||||||
|
|
||||||
# Verify
|
|
||||||
uv run fuzzforge mcp status
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**After installation:**
|
**After installation:** Restart Claude Desktop.
|
||||||
1. Restart Claude Desktop
|
|
||||||
2. FuzzForge tools are now available!
|
|
||||||
|
|
||||||
### Check MCP Status
|
### Check Status
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run fuzzforge mcp status
|
uv run fuzzforge mcp status
|
||||||
```
|
```
|
||||||
|
|
||||||
Shows configuration status for all supported AI agents:
|
### Remove Configuration
|
||||||
|
|
||||||
```
|
|
||||||
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
||||||
┃ Agent ┃ Config Path ┃ Status ┃ FuzzForge Configured ┃
|
|
||||||
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
|
||||||
│ GitHub Copilot │ ~/.config/Code/User/mcp.json │ ✓ Exists │ ✓ Yes │
|
|
||||||
│ Claude Desktop │ ~/.config/Claude/claude_desktop_config... │ Not found │ - │
|
|
||||||
│ Claude Code │ ~/.claude.json │ ✓ Exists │ ✓ Yes │
|
|
||||||
└──────────────────────┴───────────────────────────────────────────┴──────────────┴─────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generate Config Without Installing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Preview the configuration that would be installed
|
|
||||||
uv run fuzzforge mcp generate copilot
|
|
||||||
uv run fuzzforge mcp generate claude-desktop
|
|
||||||
uv run fuzzforge mcp generate claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove MCP Configuration
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run fuzzforge mcp uninstall copilot
|
uv run fuzzforge mcp uninstall copilot
|
||||||
uv run fuzzforge mcp uninstall claude-desktop
|
|
||||||
uv run fuzzforge mcp uninstall claude-code
|
uv run fuzzforge mcp uninstall claude-code
|
||||||
|
uv run fuzzforge mcp uninstall claude-desktop
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Using FuzzForge with AI
|
## Using FuzzForge with AI
|
||||||
|
|
||||||
Once MCP is configured, you interact with FuzzForge through natural language with your AI assistant.
|
Once MCP is configured and hub images are built, interact with FuzzForge through natural language with your AI assistant.
|
||||||
|
|
||||||
### Example Conversations
|
### Example Conversations
|
||||||
|
|
||||||
**Discover available tools:**
|
**Discover available tools:**
|
||||||
```
|
```
|
||||||
You: "What FuzzForge modules are available?"
|
You: "What security tools are available in FuzzForge?"
|
||||||
AI: Uses list_modules → "I found 4 modules: rust-analyzer, cargo-fuzzer,
|
AI: Queries hub tools → "I found 15 tools across categories: nmap for
|
||||||
harness-validator, and crash-analyzer..."
|
port scanning, binwalk for firmware analysis, semgrep for code
|
||||||
|
scanning, cargo-fuzzer for Rust fuzzing..."
|
||||||
```
|
```
|
||||||
|
|
||||||
**Analyze code for fuzzing targets:**
|
**Analyze a binary:**
|
||||||
|
```
|
||||||
|
You: "Extract and analyze this firmware image"
|
||||||
|
AI: Uses binwalk to extract → yara for pattern matching → capa for
|
||||||
|
capability detection → "Found 3 embedded filesystems, 2 YARA
|
||||||
|
matches for known vulnerabilities..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fuzz Rust code:**
|
||||||
```
|
```
|
||||||
You: "Analyze this Rust crate for functions I should fuzz"
|
You: "Analyze this Rust crate for functions I should fuzz"
|
||||||
AI: Uses execute_module("rust-analyzer") → "I found 3 good fuzzing candidates:
|
AI: Uses rust-analyzer → "Found 3 fuzzable entry points..."
|
||||||
- parse_input() in src/parser.rs - handles untrusted input
|
|
||||||
- decode_message() in src/codec.rs - complex parsing logic
|
|
||||||
..."
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generate and validate harnesses:**
|
|
||||||
```
|
|
||||||
You: "Generate a fuzzing harness for the parse_input function"
|
|
||||||
AI: Creates harness code, then uses execute_module("harness-validator")
|
|
||||||
→ "Here's a harness that compiles successfully..."
|
|
||||||
```
|
|
||||||
|
|
||||||
**Run continuous fuzzing:**
|
|
||||||
```
|
|
||||||
You: "Start fuzzing parse_input for 10 minutes"
|
You: "Start fuzzing parse_input for 10 minutes"
|
||||||
AI: Uses start_continuous_module("cargo-fuzzer") → "Started fuzzing session abc123"
|
AI: Uses cargo-fuzzer → "Fuzzing session started. 2 crashes found..."
|
||||||
|
|
||||||
You: "How's the fuzzing going?"
|
|
||||||
AI: Uses get_continuous_status("abc123") → "Running for 5 minutes:
|
|
||||||
- 150,000 executions
|
|
||||||
- 2 crashes found
|
|
||||||
- 45% edge coverage"
|
|
||||||
|
|
||||||
You: "Stop and show me the crashes"
|
|
||||||
AI: Uses stop_continuous_module("abc123") → "Found 2 unique crashes..."
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available MCP Tools
|
**Scan for vulnerabilities:**
|
||||||
|
```
|
||||||
| Tool | Description |
|
You: "Scan this codebase with semgrep for security issues"
|
||||||
|------|-------------|
|
AI: Uses semgrep-mcp → "Found 5 findings: 2 high severity SQL injection
|
||||||
| `list_modules` | List all available security modules |
|
patterns, 3 medium severity hardcoded secrets..."
|
||||||
| `execute_module` | Run a module once and get results |
|
```
|
||||||
| `start_continuous_module` | Start a long-running module (e.g., fuzzing) |
|
|
||||||
| `get_continuous_status` | Check status of a continuous session |
|
|
||||||
| `stop_continuous_module` | Stop a continuous session |
|
|
||||||
| `list_continuous_sessions` | List all active sessions |
|
|
||||||
| `get_execution_results` | Retrieve results from an execution |
|
|
||||||
| `execute_workflow` | Run a multi-step workflow |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## CLI Reference
|
## CLI Reference
|
||||||
|
|
||||||
> **Note:** The CLI is for advanced users. Most users should interact with FuzzForge through their AI assistant.
|
### UI Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run fuzzforge ui # Launch the terminal dashboard
|
||||||
|
```
|
||||||
|
|
||||||
### MCP Commands
|
### MCP Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run fuzzforge mcp status # Check configuration status
|
uv run fuzzforge mcp status # Check agent configuration status
|
||||||
uv run fuzzforge mcp install <agent> # Install MCP config
|
uv run fuzzforge mcp install <agent> # Install MCP config (copilot|claude-code|claude-desktop)
|
||||||
uv run fuzzforge mcp uninstall <agent> # Remove MCP config
|
uv run fuzzforge mcp uninstall <agent> # Remove MCP config
|
||||||
uv run fuzzforge mcp generate <agent> # Preview config without installing
|
uv run fuzzforge mcp generate <agent> # Preview config without installing
|
||||||
```
|
```
|
||||||
|
|
||||||
### Module Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run fuzzforge modules list # List available modules
|
|
||||||
uv run fuzzforge modules info <module> # Show module details
|
|
||||||
uv run fuzzforge modules run <module> --assets . # Run a module
|
|
||||||
```
|
|
||||||
|
|
||||||
### Project Commands
|
### Project Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -343,14 +399,13 @@ uv run fuzzforge project results <id> # Get execution results
|
|||||||
Configure FuzzForge using environment variables:
|
Configure FuzzForge using environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Project paths
|
# Storage path for projects and execution results
|
||||||
export FUZZFORGE_MODULES_PATH=/path/to/modules
|
|
||||||
export FUZZFORGE_STORAGE_PATH=/path/to/storage
|
export FUZZFORGE_STORAGE_PATH=/path/to/storage
|
||||||
|
|
||||||
# Container engine (Docker is default)
|
# Container engine (Docker is default)
|
||||||
export FUZZFORGE_ENGINE__TYPE=docker # or podman
|
export FUZZFORGE_ENGINE__TYPE=docker # or podman
|
||||||
|
|
||||||
# Podman-specific settings (only needed if using Podman under Snap)
|
# Podman-specific settings
|
||||||
export FUZZFORGE_ENGINE__GRAPHROOT=~/.fuzzforge/containers/storage
|
export FUZZFORGE_ENGINE__GRAPHROOT=~/.fuzzforge/containers/storage
|
||||||
export FUZZFORGE_ENGINE__RUNROOT=~/.fuzzforge/containers/run
|
export FUZZFORGE_ENGINE__RUNROOT=~/.fuzzforge/containers/run
|
||||||
```
|
```
|
||||||
@@ -384,66 +439,62 @@ Error: Permission denied connecting to Docker socket
|
|||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
```bash
|
```bash
|
||||||
# Add your user to the docker group
|
|
||||||
sudo usermod -aG docker $USER
|
sudo usermod -aG docker $USER
|
||||||
|
# Log out and back in, then verify:
|
||||||
# Log out and back in for changes to take effect
|
|
||||||
# Then verify:
|
|
||||||
docker run --rm hello-world
|
docker run --rm hello-world
|
||||||
```
|
```
|
||||||
|
|
||||||
### No Modules Found
|
### Hub Images Not Built
|
||||||
|
|
||||||
```
|
The dashboard shows ✗ Not built for tools:
|
||||||
No modules found.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution:**
|
```bash
|
||||||
1. Build the modules first: `make build-modules`
|
# Build all hub images
|
||||||
2. Check the modules path: `uv run fuzzforge modules list`
|
./scripts/build-hub-images.sh
|
||||||
3. Verify images exist: `docker images | grep fuzzforge`
|
|
||||||
|
# Or build a single tool
|
||||||
|
docker build -t <tool-name>:latest mcp-security-hub/<category>/<tool-name>/
|
||||||
|
```
|
||||||
|
|
||||||
### MCP Server Not Starting
|
### MCP Server Not Starting
|
||||||
|
|
||||||
Check the MCP configuration:
|
|
||||||
```bash
|
```bash
|
||||||
|
# Check agent configuration
|
||||||
uv run fuzzforge mcp status
|
uv run fuzzforge mcp status
|
||||||
```
|
|
||||||
|
|
||||||
Verify the configuration file path exists and contains valid JSON.
|
# Verify the config file path exists and contains valid JSON
|
||||||
|
cat ~/.config/Code/User/mcp.json # Copilot
|
||||||
### Module Container Fails to Build
|
cat ~/.claude.json # Claude Code
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build module container manually to see errors
|
|
||||||
cd fuzzforge-modules/<module-name>
|
|
||||||
docker build -t <module-name> .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Podman Instead of Docker
|
### Using Podman Instead of Docker
|
||||||
|
|
||||||
If you prefer Podman:
|
|
||||||
```bash
|
```bash
|
||||||
# Use --engine podman with CLI
|
# Install with Podman engine
|
||||||
uv run fuzzforge mcp install copilot --engine podman
|
uv run fuzzforge mcp install copilot --engine podman
|
||||||
|
|
||||||
# Or set environment variable
|
# Or set environment variable
|
||||||
export FUZZFORGE_ENGINE=podman
|
export FUZZFORGE_ENGINE=podman
|
||||||
```
|
```
|
||||||
|
|
||||||
### Check Logs
|
### Hub Registry
|
||||||
|
|
||||||
|
FuzzForge stores linked hub information in `~/.fuzzforge/hubs.json`. If something goes wrong:
|
||||||
|
|
||||||
FuzzForge stores execution logs in the storage directory:
|
|
||||||
```bash
|
```bash
|
||||||
ls -la ~/.fuzzforge/storage/<project-id>/<execution-id>/
|
# View registry
|
||||||
|
cat ~/.fuzzforge/hubs.json
|
||||||
|
|
||||||
|
# Reset registry
|
||||||
|
rm ~/.fuzzforge/hubs.json
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- 📖 Read the [Module SDK Guide](fuzzforge-modules/fuzzforge-modules-sdk/README.md) to create custom modules
|
- 🖥️ Launch `uv run fuzzforge ui` and explore the dashboard
|
||||||
- 🎬 Check the demos in the [README](README.md)
|
- 🔒 Clone the [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub) for 40+ security tools
|
||||||
- 💬 Join our [Discord](https://discord.gg/8XEX33UUwZ) for support
|
- 💬 Join our [Discord](https://discord.gg/8XEX33UUwZ) for support
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ requires-python = ">=3.14"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"fuzzforge-mcp==0.0.1",
|
"fuzzforge-mcp==0.0.1",
|
||||||
"rich>=14.0.0",
|
"rich>=14.0.0",
|
||||||
|
"textual>=1.0.0",
|
||||||
"typer==0.20.1",
|
"typer==0.20.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def main(
|
|||||||
envvar="FUZZFORGE_STORAGE__PATH",
|
envvar="FUZZFORGE_STORAGE__PATH",
|
||||||
help="Path to the storage directory.",
|
help="Path to the storage directory.",
|
||||||
),
|
),
|
||||||
] = Path.home() / ".fuzzforge" / "storage",
|
] = Path.cwd() / ".fuzzforge" / "storage",
|
||||||
context: TyperContext = None, # type: ignore[assignment]
|
context: TyperContext = None, # type: ignore[assignment]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""FuzzForge AI - Security research orchestration platform.
|
"""FuzzForge AI - Security research orchestration platform.
|
||||||
@@ -42,7 +42,7 @@ def main(
|
|||||||
Discover and execute MCP hub tools for security research.
|
Discover and execute MCP hub tools for security research.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
storage = LocalStorage(storage_path=storage_path)
|
storage = LocalStorage(base_path=storage_path)
|
||||||
|
|
||||||
context.obj = Context(
|
context.obj = Context(
|
||||||
storage=storage,
|
storage=storage,
|
||||||
@@ -52,3 +52,19 @@ def main(
|
|||||||
|
|
||||||
application.add_typer(mcp.application)
|
application.add_typer(mcp.application)
|
||||||
application.add_typer(projects.application)
|
application.add_typer(projects.application)
|
||||||
|
|
||||||
|
|
||||||
|
@application.command(
|
||||||
|
name="ui",
|
||||||
|
help="Launch the FuzzForge terminal interface.",
|
||||||
|
)
|
||||||
|
def launch_ui() -> None:
|
||||||
|
"""Launch the interactive FuzzForge TUI dashboard.
|
||||||
|
|
||||||
|
Provides a visual dashboard showing AI agent connection status
|
||||||
|
and hub server availability, with wizards for setup and configuration.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from fuzzforge_cli.tui.app import FuzzForgeApp
|
||||||
|
|
||||||
|
FuzzForgeApp().run()
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ def _generate_mcp_config(
|
|||||||
|
|
||||||
# Self-contained storage paths for FuzzForge containers
|
# Self-contained storage paths for FuzzForge containers
|
||||||
# This isolates FuzzForge from system Podman and avoids snap issues
|
# This isolates FuzzForge from system Podman and avoids snap issues
|
||||||
fuzzforge_home = Path.home() / ".fuzzforge"
|
fuzzforge_home = Path.cwd() / ".fuzzforge"
|
||||||
graphroot = fuzzforge_home / "containers" / "storage"
|
graphroot = fuzzforge_home / "containers" / "storage"
|
||||||
runroot = fuzzforge_home / "containers" / "run"
|
runroot = fuzzforge_home / "containers" / "run"
|
||||||
|
|
||||||
|
|||||||
1
fuzzforge-cli/src/fuzzforge_cli/tui/__init__.py
Normal file
1
fuzzforge-cli/src/fuzzforge_cli/tui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""FuzzForge terminal user interface."""
|
||||||
360
fuzzforge-cli/src/fuzzforge_cli/tui/app.py
Normal file
360
fuzzforge-cli/src/fuzzforge_cli/tui/app.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
"""FuzzForge TUI application.
|
||||||
|
|
||||||
|
Main terminal user interface for FuzzForge, providing a dashboard
|
||||||
|
with AI agent connection status, hub server availability, and
|
||||||
|
hub management capabilities.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.binding import Binding
|
||||||
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||||
|
from textual.widgets import Button, DataTable, Footer, Header, Label
|
||||||
|
|
||||||
|
from fuzzforge_cli.tui.helpers import (
|
||||||
|
check_agent_status,
|
||||||
|
check_hub_image,
|
||||||
|
find_fuzzforge_root,
|
||||||
|
get_agent_configs,
|
||||||
|
load_hub_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Agent config entries stored alongside their linked status for row mapping
|
||||||
|
_AgentRow = tuple[str, "AIAgent", "Path", str, bool] # noqa: F821
|
||||||
|
|
||||||
|
|
||||||
|
class FuzzForgeApp(App):
|
||||||
|
"""FuzzForge AI terminal user interface."""
|
||||||
|
|
||||||
|
TITLE = "FuzzForge AI"
|
||||||
|
SUB_TITLE = "Security Research Orchestration"
|
||||||
|
|
||||||
|
CSS = """
|
||||||
|
Screen {
|
||||||
|
background: $surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
height: 1fr;
|
||||||
|
margin: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
width: 1fr;
|
||||||
|
border: round #4699fc;
|
||||||
|
padding: 1 2;
|
||||||
|
margin: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hub-panel {
|
||||||
|
height: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hub-table {
|
||||||
|
height: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agents-panel {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
text-style: bold;
|
||||||
|
color: #4699fc;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hub-title-bar {
|
||||||
|
height: auto;
|
||||||
|
align: center middle;
|
||||||
|
margin: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-hub-manager {
|
||||||
|
min-width: 40;
|
||||||
|
margin-right: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-fuzzinglabs-hub {
|
||||||
|
min-width: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
#agents-table {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal screens */
|
||||||
|
AgentSetupScreen, AgentUnlinkScreen,
|
||||||
|
HubManagerScreen, LinkHubScreen, CloneHubScreen {
|
||||||
|
align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setup-dialog, #unlink-dialog {
|
||||||
|
width: 56;
|
||||||
|
height: auto;
|
||||||
|
max-height: 80%;
|
||||||
|
border: thick #4699fc;
|
||||||
|
background: $surface;
|
||||||
|
padding: 2 3;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hub-manager-dialog {
|
||||||
|
width: 100;
|
||||||
|
height: auto;
|
||||||
|
max-height: 85%;
|
||||||
|
border: thick #4699fc;
|
||||||
|
background: $surface;
|
||||||
|
padding: 2 3;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#link-dialog, #clone-dialog {
|
||||||
|
width: 72;
|
||||||
|
height: auto;
|
||||||
|
max-height: 80%;
|
||||||
|
border: thick #4699fc;
|
||||||
|
background: $surface;
|
||||||
|
padding: 2 3;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
text-style: bold;
|
||||||
|
text-align: center;
|
||||||
|
color: #4699fc;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
margin-top: 1;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioSet {
|
||||||
|
height: auto;
|
||||||
|
margin: 0 0 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
margin: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons {
|
||||||
|
layout: horizontal;
|
||||||
|
height: 3;
|
||||||
|
align: center middle;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-buttons Button {
|
||||||
|
margin: 0 1;
|
||||||
|
min-width: 14;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
BINDINGS = [
|
||||||
|
Binding("q", "quit", "Quit"),
|
||||||
|
Binding("h", "manage_hubs", "Hub Manager"),
|
||||||
|
Binding("r", "refresh", "Refresh"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the dashboard layout."""
|
||||||
|
yield Header()
|
||||||
|
with VerticalScroll(id="main"):
|
||||||
|
with Vertical(id="hub-panel", classes="panel"):
|
||||||
|
yield DataTable(id="hub-table")
|
||||||
|
with Horizontal(id="hub-title-bar"):
|
||||||
|
yield Button(
|
||||||
|
"Hub Manager (h)",
|
||||||
|
variant="primary",
|
||||||
|
id="btn-hub-manager",
|
||||||
|
)
|
||||||
|
yield Button(
|
||||||
|
"FuzzingLabs Hub",
|
||||||
|
variant="primary",
|
||||||
|
id="btn-fuzzinglabs-hub",
|
||||||
|
)
|
||||||
|
with Vertical(id="agents-panel", classes="panel"):
|
||||||
|
yield DataTable(id="agents-table")
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Populate tables on startup."""
|
||||||
|
self._agent_rows: list[_AgentRow] = []
|
||||||
|
self.query_one("#hub-panel").border_title = "Hub Servers"
|
||||||
|
self.query_one("#agents-panel").border_title = "AI Agents"
|
||||||
|
self._refresh_agents()
|
||||||
|
self._refresh_hub()
|
||||||
|
|
||||||
|
def _refresh_agents(self) -> None:
|
||||||
|
"""Refresh the AI agents status table."""
|
||||||
|
table = self.query_one("#agents-table", DataTable)
|
||||||
|
table.clear(columns=True)
|
||||||
|
table.add_columns("Agent", "Status", "Config Path")
|
||||||
|
table.cursor_type = "row"
|
||||||
|
|
||||||
|
self._agent_rows = []
|
||||||
|
for display_name, agent, config_path, servers_key in get_agent_configs():
|
||||||
|
is_linked, status_text = check_agent_status(config_path, servers_key)
|
||||||
|
if is_linked:
|
||||||
|
status_cell = Text(f"✓ {status_text}", style="green")
|
||||||
|
else:
|
||||||
|
status_cell = Text(f"✗ {status_text}", style="red")
|
||||||
|
table.add_row(display_name, status_cell, str(config_path))
|
||||||
|
self._agent_rows.append(
|
||||||
|
(display_name, agent, config_path, servers_key, is_linked)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_hub(self) -> None:
|
||||||
|
"""Refresh the hub servers table, grouped by source hub."""
|
||||||
|
table = self.query_one("#hub-table", DataTable)
|
||||||
|
table.clear(columns=True)
|
||||||
|
table.add_columns("Server", "Image", "Hub", "Status")
|
||||||
|
|
||||||
|
try:
|
||||||
|
fuzzforge_root = find_fuzzforge_root()
|
||||||
|
hub_config = load_hub_config(fuzzforge_root)
|
||||||
|
except Exception:
|
||||||
|
table.add_row(
|
||||||
|
Text("Error loading config", style="red"), "", "", ""
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
servers = hub_config.get("servers", [])
|
||||||
|
if not servers:
|
||||||
|
table.add_row(
|
||||||
|
Text("No servers — press h", style="dim"), "", "", ""
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Group servers by source hub
|
||||||
|
groups: dict[str, list[dict]] = defaultdict(list)
|
||||||
|
for server in servers:
|
||||||
|
source = server.get("source_hub", "manual")
|
||||||
|
groups[source].append(server)
|
||||||
|
|
||||||
|
for hub_name, hub_servers in groups.items():
|
||||||
|
ready_count = 0
|
||||||
|
total = len(hub_servers)
|
||||||
|
|
||||||
|
statuses: list[tuple[dict, bool, str]] = []
|
||||||
|
for server in hub_servers:
|
||||||
|
enabled = server.get("enabled", True)
|
||||||
|
if not enabled:
|
||||||
|
statuses.append((server, False, "Disabled"))
|
||||||
|
else:
|
||||||
|
is_ready, status_text = check_hub_image(
|
||||||
|
server.get("image", "")
|
||||||
|
)
|
||||||
|
if is_ready:
|
||||||
|
ready_count += 1
|
||||||
|
statuses.append((server, is_ready, status_text))
|
||||||
|
|
||||||
|
# Group header row
|
||||||
|
if hub_name == "manual":
|
||||||
|
header = Text(
|
||||||
|
f"▼ 📦 Local config ({ready_count}/{total} ready)",
|
||||||
|
style="bold",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header = Text(
|
||||||
|
f"▼ 🔗 {hub_name} ({ready_count}/{total} ready)",
|
||||||
|
style="bold",
|
||||||
|
)
|
||||||
|
table.add_row(header, "", "", "")
|
||||||
|
|
||||||
|
# Tool rows
|
||||||
|
for server, is_ready, status_text in statuses:
|
||||||
|
name = server.get("name", "unknown")
|
||||||
|
image = server.get("image", "unknown")
|
||||||
|
enabled = server.get("enabled", True)
|
||||||
|
|
||||||
|
if not enabled:
|
||||||
|
status_cell = Text("Disabled", style="dim")
|
||||||
|
elif is_ready:
|
||||||
|
status_cell = Text("✓ Ready", style="green")
|
||||||
|
else:
|
||||||
|
status_cell = Text(f"✗ {status_text}", style="red")
|
||||||
|
|
||||||
|
table.add_row(
|
||||||
|
f" {name}",
|
||||||
|
Text(image, style="dim"),
|
||||||
|
hub_name,
|
||||||
|
status_cell,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||||
|
"""Handle row selection on the agents table."""
|
||||||
|
if event.data_table.id != "agents-table":
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = event.cursor_row
|
||||||
|
if idx < 0 or idx >= len(self._agent_rows):
|
||||||
|
return
|
||||||
|
|
||||||
|
display_name, agent, _config_path, _servers_key, is_linked = self._agent_rows[idx]
|
||||||
|
|
||||||
|
if is_linked:
|
||||||
|
from fuzzforge_cli.tui.screens.agent_setup import AgentUnlinkScreen
|
||||||
|
|
||||||
|
self.push_screen(
|
||||||
|
AgentUnlinkScreen(agent, display_name),
|
||||||
|
callback=self._on_agent_changed,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from fuzzforge_cli.tui.screens.agent_setup import AgentSetupScreen
|
||||||
|
|
||||||
|
self.push_screen(
|
||||||
|
AgentSetupScreen(agent, display_name),
|
||||||
|
callback=self._on_agent_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button presses."""
|
||||||
|
if event.button.id == "btn-hub-manager":
|
||||||
|
self.action_manage_hubs()
|
||||||
|
elif event.button.id == "btn-fuzzinglabs-hub":
|
||||||
|
self.action_add_fuzzinglabs_hub()
|
||||||
|
|
||||||
|
def action_add_fuzzinglabs_hub(self) -> None:
|
||||||
|
"""Open the clone dialog pre-filled with the FuzzingLabs hub URL."""
|
||||||
|
from fuzzforge_cli.tui.screens.hub_manager import CloneHubScreen
|
||||||
|
|
||||||
|
self.push_screen(
|
||||||
|
CloneHubScreen(
|
||||||
|
default_url="https://github.com/FuzzingLabs/mcp-security-hub",
|
||||||
|
default_name="mcp-security-hub",
|
||||||
|
is_default=True,
|
||||||
|
),
|
||||||
|
callback=self._on_hub_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_manage_hubs(self) -> None:
|
||||||
|
"""Open the hub manager."""
|
||||||
|
from fuzzforge_cli.tui.screens.hub_manager import HubManagerScreen
|
||||||
|
|
||||||
|
self.push_screen(HubManagerScreen(), callback=self._on_hub_changed)
|
||||||
|
|
||||||
|
def _on_agent_changed(self, result: str | None) -> None:
|
||||||
|
"""Handle agent setup/unlink completion."""
|
||||||
|
if result:
|
||||||
|
self.notify(result)
|
||||||
|
self._refresh_agents()
|
||||||
|
|
||||||
|
def _on_hub_changed(self, result: str | None) -> None:
|
||||||
|
"""Handle hub manager completion — refresh the hub table."""
|
||||||
|
self._refresh_hub()
|
||||||
|
|
||||||
|
def action_refresh(self) -> None:
|
||||||
|
"""Refresh all status panels."""
|
||||||
|
self._refresh_agents()
|
||||||
|
self._refresh_hub()
|
||||||
|
self.notify("Status refreshed")
|
||||||
535
fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py
Normal file
535
fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
"""Shared helpers for FuzzForge TUI and CLI.
|
||||||
|
|
||||||
|
Provides utility functions for checking AI agent configuration status,
|
||||||
|
hub server image availability, installing/removing MCP configurations,
|
||||||
|
and managing linked MCP hub repositories.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fuzzforge_cli.commands.mcp import (
|
||||||
|
AIAgent,
|
||||||
|
_detect_docker_socket,
|
||||||
|
_detect_podman_socket,
|
||||||
|
_find_fuzzforge_root,
|
||||||
|
_generate_mcp_config,
|
||||||
|
_get_claude_code_user_mcp_path,
|
||||||
|
_get_claude_desktop_mcp_path,
|
||||||
|
_get_copilot_mcp_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Hub Management Constants ---
|
||||||
|
|
||||||
|
FUZZFORGE_DEFAULT_HUB_URL = "git@github.com:FuzzingLabs/mcp-security-hub.git"
|
||||||
|
FUZZFORGE_DEFAULT_HUB_NAME = "mcp-security-hub"
|
||||||
|
|
||||||
|
|
||||||
|
def get_fuzzforge_dir() -> Path:
|
||||||
|
"""Return the project-local ``.fuzzforge/`` directory.
|
||||||
|
|
||||||
|
Uses the current working directory so that each project gets its
|
||||||
|
own isolated FuzzForge configuration, hubs, and storage — similar
|
||||||
|
to how ``.git/`` or ``.venv/`` work.
|
||||||
|
|
||||||
|
:return: ``Path.cwd() / ".fuzzforge"``
|
||||||
|
|
||||||
|
"""
|
||||||
|
return Path.cwd() / ".fuzzforge"
|
||||||
|
|
||||||
|
# Categories that typically need NET_RAW capability for network access
|
||||||
|
_NET_RAW_CATEGORIES = {"reconnaissance", "web-security"}
|
||||||
|
|
||||||
|
# Directories to skip when scanning a hub for MCP tool Dockerfiles
|
||||||
|
_SCAN_SKIP_DIRS = {
|
||||||
|
".git",
|
||||||
|
".github",
|
||||||
|
"scripts",
|
||||||
|
"tests",
|
||||||
|
"examples",
|
||||||
|
"meta",
|
||||||
|
"__pycache__",
|
||||||
|
"node_modules",
|
||||||
|
".venv",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_agent_configs() -> list[tuple[str, AIAgent, Path, str]]:
|
||||||
|
"""Return agent display configs with resolved paths.
|
||||||
|
|
||||||
|
Each tuple contains:
|
||||||
|
- Display name
|
||||||
|
- AIAgent enum value
|
||||||
|
- Config file path
|
||||||
|
- Servers JSON key
|
||||||
|
|
||||||
|
:return: List of agent configuration tuples.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
("GitHub Copilot", AIAgent.COPILOT, _get_copilot_mcp_path(), "servers"),
|
||||||
|
("Claude Desktop", AIAgent.CLAUDE_DESKTOP, _get_claude_desktop_mcp_path(), "mcpServers"),
|
||||||
|
("Claude Code", AIAgent.CLAUDE_CODE, _get_claude_code_user_mcp_path(), "mcpServers"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def check_agent_status(config_path: Path, servers_key: str) -> tuple[bool, str]:
|
||||||
|
"""Check whether an AI agent has FuzzForge configured.
|
||||||
|
|
||||||
|
:param config_path: Path to the agent's MCP config file.
|
||||||
|
:param servers_key: JSON key for the servers dict (e.g. "servers" or "mcpServers").
|
||||||
|
:return: Tuple of (is_linked, status_description).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not config_path.exists():
|
||||||
|
return False, "Not configured"
|
||||||
|
try:
|
||||||
|
config = json.loads(config_path.read_text())
|
||||||
|
servers = config.get(servers_key, {})
|
||||||
|
if "fuzzforge" in servers:
|
||||||
|
return True, "Linked"
|
||||||
|
return False, "Config exists, not linked"
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return False, "Invalid config file"
|
||||||
|
|
||||||
|
|
||||||
|
def check_hub_image(image: str) -> tuple[bool, str]:
|
||||||
|
"""Check whether a Docker image exists locally.
|
||||||
|
|
||||||
|
:param image: Docker image name (e.g. "semgrep-mcp:latest").
|
||||||
|
:return: Tuple of (is_ready, status_description).
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "image", "inspect", image],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, "Ready"
|
||||||
|
return False, "Not built"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Timeout"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "Docker not found"
|
||||||
|
|
||||||
|
|
||||||
|
def load_hub_config(fuzzforge_root: Path) -> dict[str, Any]:
|
||||||
|
"""Load hub-config.json from the FuzzForge root.
|
||||||
|
|
||||||
|
:param fuzzforge_root: Path to fuzzforge-oss directory.
|
||||||
|
:return: Parsed hub configuration dict, empty dict on error.
|
||||||
|
|
||||||
|
"""
|
||||||
|
config_path = fuzzforge_root / "hub-config.json"
|
||||||
|
if not config_path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(config_path.read_text())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def find_fuzzforge_root() -> Path:
|
||||||
|
"""Find the FuzzForge installation root directory.
|
||||||
|
|
||||||
|
:return: Path to the fuzzforge-oss directory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _find_fuzzforge_root()
|
||||||
|
|
||||||
|
|
||||||
|
def install_agent_config(agent: AIAgent, engine: str, force: bool = False) -> str:
|
||||||
|
"""Install FuzzForge MCP configuration for an AI agent.
|
||||||
|
|
||||||
|
:param agent: Target AI agent.
|
||||||
|
:param engine: Container engine type ("docker" or "podman").
|
||||||
|
:param force: Overwrite existing configuration.
|
||||||
|
:return: Result message string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
fuzzforge_root = _find_fuzzforge_root()
|
||||||
|
|
||||||
|
if agent == AIAgent.COPILOT:
|
||||||
|
config_path = _get_copilot_mcp_path()
|
||||||
|
servers_key = "servers"
|
||||||
|
elif agent == AIAgent.CLAUDE_CODE:
|
||||||
|
config_path = _get_claude_code_user_mcp_path()
|
||||||
|
servers_key = "mcpServers"
|
||||||
|
else:
|
||||||
|
config_path = _get_claude_desktop_mcp_path()
|
||||||
|
servers_key = "mcpServers"
|
||||||
|
|
||||||
|
socket = _detect_docker_socket() if engine == "docker" else _detect_podman_socket()
|
||||||
|
|
||||||
|
server_config = _generate_mcp_config(
|
||||||
|
fuzzforge_root=fuzzforge_root,
|
||||||
|
engine_type=engine,
|
||||||
|
engine_socket=socket,
|
||||||
|
)
|
||||||
|
|
||||||
|
if config_path.exists():
|
||||||
|
try:
|
||||||
|
existing = json.loads(config_path.read_text())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return f"Error: Invalid JSON in {config_path}"
|
||||||
|
|
||||||
|
servers = existing.get(servers_key, {})
|
||||||
|
if "fuzzforge" in servers and not force:
|
||||||
|
return "Already configured (use force to overwrite)"
|
||||||
|
|
||||||
|
if servers_key not in existing:
|
||||||
|
existing[servers_key] = {}
|
||||||
|
existing[servers_key]["fuzzforge"] = server_config
|
||||||
|
full_config = existing
|
||||||
|
else:
|
||||||
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
full_config = {servers_key: {"fuzzforge": server_config}}
|
||||||
|
|
||||||
|
config_path.write_text(json.dumps(full_config, indent=4))
|
||||||
|
return f"Installed FuzzForge for {agent.value}"
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall_agent_config(agent: AIAgent) -> str:
|
||||||
|
"""Remove FuzzForge MCP configuration from an AI agent.
|
||||||
|
|
||||||
|
:param agent: Target AI agent.
|
||||||
|
:return: Result message string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if agent == AIAgent.COPILOT:
|
||||||
|
config_path = _get_copilot_mcp_path()
|
||||||
|
servers_key = "servers"
|
||||||
|
elif agent == AIAgent.CLAUDE_CODE:
|
||||||
|
config_path = _get_claude_code_user_mcp_path()
|
||||||
|
servers_key = "mcpServers"
|
||||||
|
else:
|
||||||
|
config_path = _get_claude_desktop_mcp_path()
|
||||||
|
servers_key = "mcpServers"
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
return "Configuration file not found"
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = json.loads(config_path.read_text())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return "Error: Invalid JSON in config file"
|
||||||
|
|
||||||
|
servers = config.get(servers_key, {})
|
||||||
|
if "fuzzforge" not in servers:
|
||||||
|
return "FuzzForge is not configured for this agent"
|
||||||
|
|
||||||
|
del servers["fuzzforge"]
|
||||||
|
config_path.write_text(json.dumps(config, indent=4))
|
||||||
|
return f"Removed FuzzForge from {agent.value}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hub Management
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def get_hubs_registry_path() -> Path:
|
||||||
|
"""Return path to the hubs registry file (``.fuzzforge/hubs.json``).
|
||||||
|
|
||||||
|
:return: Path to the registry JSON file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return get_fuzzforge_dir() / "hubs.json"
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_hubs_dir() -> Path:
|
||||||
|
"""Return default directory for cloned hubs (``.fuzzforge/hubs/``).
|
||||||
|
|
||||||
|
:return: Path to the default hubs directory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return get_fuzzforge_dir() / "hubs"
|
||||||
|
|
||||||
|
|
||||||
|
def load_hubs_registry() -> dict[str, Any]:
|
||||||
|
"""Load the hubs registry from disk.
|
||||||
|
|
||||||
|
:return: Registry dict with ``hubs`` key containing a list of hub entries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = get_hubs_registry_path()
|
||||||
|
if not path.exists():
|
||||||
|
return {"hubs": []}
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text())
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return {"hubs": []}
|
||||||
|
|
||||||
|
|
||||||
|
def save_hubs_registry(registry: dict[str, Any]) -> None:
|
||||||
|
"""Save the hubs registry to disk.
|
||||||
|
|
||||||
|
:param registry: Registry dict to persist.
|
||||||
|
|
||||||
|
"""
|
||||||
|
path = get_hubs_registry_path()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(registry, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def scan_hub_for_servers(hub_path: Path) -> list[dict[str, Any]]:
|
||||||
|
"""Scan a hub directory for MCP tool Dockerfiles.
|
||||||
|
|
||||||
|
Looks for the ``category/tool-name/Dockerfile`` pattern and generates
|
||||||
|
a server configuration entry for each discovered tool.
|
||||||
|
|
||||||
|
:param hub_path: Root directory of the hub repository.
|
||||||
|
:return: Sorted list of server configuration dicts.
|
||||||
|
|
||||||
|
"""
|
||||||
|
servers: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
if not hub_path.is_dir():
|
||||||
|
return servers
|
||||||
|
|
||||||
|
for dockerfile in sorted(hub_path.rglob("Dockerfile")):
|
||||||
|
rel = dockerfile.relative_to(hub_path)
|
||||||
|
parts = rel.parts
|
||||||
|
|
||||||
|
# Expected layout: category/tool-name/Dockerfile (exactly 3 parts)
|
||||||
|
if len(parts) != 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
category, tool_name, _ = parts
|
||||||
|
|
||||||
|
if category in _SCAN_SKIP_DIRS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
capabilities: list[str] = []
|
||||||
|
if category in _NET_RAW_CATEGORIES:
|
||||||
|
capabilities = ["NET_RAW"]
|
||||||
|
|
||||||
|
servers.append(
|
||||||
|
{
|
||||||
|
"name": tool_name,
|
||||||
|
"description": f"{tool_name} — {category}",
|
||||||
|
"type": "docker",
|
||||||
|
"image": f"{tool_name}:latest",
|
||||||
|
"category": category,
|
||||||
|
"capabilities": capabilities,
|
||||||
|
"volumes": [f"{get_fuzzforge_dir()}/hub/workspace:/data"],
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return servers
|
||||||
|
|
||||||
|
|
||||||
|
def link_hub(
|
||||||
|
name: str,
|
||||||
|
path: str | Path,
|
||||||
|
git_url: str | None = None,
|
||||||
|
is_default: bool = False,
|
||||||
|
) -> str:
|
||||||
|
"""Link a hub directory and add its servers to hub-config.json.
|
||||||
|
|
||||||
|
:param name: Display name for the hub.
|
||||||
|
:param path: Local directory path containing the hub.
|
||||||
|
:param git_url: Optional git remote URL (for tracking).
|
||||||
|
:param is_default: Whether this is the default FuzzingLabs hub.
|
||||||
|
:return: Result message string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
hub_path = Path(path).resolve()
|
||||||
|
|
||||||
|
if not hub_path.is_dir():
|
||||||
|
return f"Error: directory not found: {hub_path}"
|
||||||
|
|
||||||
|
# Update registry
|
||||||
|
registry = load_hubs_registry()
|
||||||
|
hubs = registry.get("hubs", [])
|
||||||
|
|
||||||
|
# Remove existing entry with same name
|
||||||
|
hubs = [h for h in hubs if h.get("name") != name]
|
||||||
|
|
||||||
|
hubs.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"path": str(hub_path),
|
||||||
|
"git_url": git_url,
|
||||||
|
"is_default": is_default,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
registry["hubs"] = hubs
|
||||||
|
save_hubs_registry(registry)
|
||||||
|
|
||||||
|
# Scan and update hub-config.json
|
||||||
|
scanned = scan_hub_for_servers(hub_path)
|
||||||
|
if not scanned:
|
||||||
|
return f"Linked '{name}' (0 servers found)"
|
||||||
|
|
||||||
|
try:
|
||||||
|
added = _merge_servers_into_hub_config(name, scanned)
|
||||||
|
except Exception as exc:
|
||||||
|
return f"Linked '{name}' but config update failed: {exc}"
|
||||||
|
|
||||||
|
return f"Linked '{name}' — {added} new servers added ({len(scanned)} scanned)"
|
||||||
|
|
||||||
|
|
||||||
|
def unlink_hub(name: str) -> str:
|
||||||
|
"""Unlink a hub and remove its servers from hub-config.json.
|
||||||
|
|
||||||
|
:param name: Name of the hub to unlink.
|
||||||
|
:return: Result message string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
registry = load_hubs_registry()
|
||||||
|
hubs = registry.get("hubs", [])
|
||||||
|
|
||||||
|
if not any(h.get("name") == name for h in hubs):
|
||||||
|
return f"Hub '{name}' is not linked"
|
||||||
|
|
||||||
|
hubs = [h for h in hubs if h.get("name") != name]
|
||||||
|
registry["hubs"] = hubs
|
||||||
|
save_hubs_registry(registry)
|
||||||
|
|
||||||
|
try:
|
||||||
|
removed = _remove_hub_servers_from_config(name)
|
||||||
|
except Exception:
|
||||||
|
removed = 0
|
||||||
|
|
||||||
|
return f"Unlinked '{name}' — {removed} server(s) removed"
|
||||||
|
|
||||||
|
|
||||||
|
def clone_hub(
|
||||||
|
git_url: str,
|
||||||
|
dest: Path | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
) -> tuple[bool, str, Path | None]:
|
||||||
|
"""Clone a git hub repository.
|
||||||
|
|
||||||
|
If the destination already exists and is a git repo, pulls instead.
|
||||||
|
|
||||||
|
:param git_url: Git remote URL to clone.
|
||||||
|
:param dest: Destination directory (auto-derived from URL if *None*).
|
||||||
|
:param name: Hub name (auto-derived from URL if *None*).
|
||||||
|
:return: Tuple of ``(success, message, clone_path)``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
name = git_url.rstrip("/").split("/")[-1]
|
||||||
|
if name.endswith(".git"):
|
||||||
|
name = name[:-4]
|
||||||
|
|
||||||
|
if dest is None:
|
||||||
|
dest = get_default_hubs_dir() / name
|
||||||
|
|
||||||
|
if dest.exists():
|
||||||
|
if (dest / ".git").is_dir():
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "-C", str(dest), "pull"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, f"Updated existing clone at {dest}", dest
|
||||||
|
return False, f"Git pull failed: {result.stderr.strip()}", None
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Git pull timed out", None
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "Git not found", None
|
||||||
|
return False, f"Directory already exists (not a git repo): {dest}", None
|
||||||
|
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "clone", git_url, str(dest)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, f"Cloned to {dest}", dest
|
||||||
|
return False, f"Git clone failed: {result.stderr.strip()}", None
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return False, "Git clone timed out (5 min limit)", None
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "Git not found on PATH", None
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_servers_into_hub_config(
|
||||||
|
hub_name: str,
|
||||||
|
servers: list[dict[str, Any]],
|
||||||
|
) -> int:
|
||||||
|
"""Merge scanned servers into hub-config.json.
|
||||||
|
|
||||||
|
Only adds servers whose name does not already exist in the config.
|
||||||
|
New entries are tagged with ``source_hub`` for later removal.
|
||||||
|
|
||||||
|
:param hub_name: Name of the source hub (used for tagging).
|
||||||
|
:param servers: List of server dicts from :func:`scan_hub_for_servers`.
|
||||||
|
:return: Number of newly added servers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
fuzzforge_root = find_fuzzforge_root()
|
||||||
|
config_path = fuzzforge_root / "hub-config.json"
|
||||||
|
|
||||||
|
if config_path.exists():
|
||||||
|
try:
|
||||||
|
config = json.loads(config_path.read_text())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
config = {"servers": [], "default_timeout": 300, "cache_tools": True}
|
||||||
|
else:
|
||||||
|
config = {"servers": [], "default_timeout": 300, "cache_tools": True}
|
||||||
|
|
||||||
|
existing = config.get("servers", [])
|
||||||
|
existing_names = {s.get("name") for s in existing}
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
for server in servers:
|
||||||
|
if server["name"] not in existing_names:
|
||||||
|
server["source_hub"] = hub_name
|
||||||
|
existing.append(server)
|
||||||
|
existing_names.add(server["name"])
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
config["servers"] = existing
|
||||||
|
config_path.write_text(json.dumps(config, indent=2))
|
||||||
|
return added
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_hub_servers_from_config(hub_name: str) -> int:
|
||||||
|
"""Remove servers belonging to a hub from hub-config.json.
|
||||||
|
|
||||||
|
Only removes servers tagged with the given ``source_hub`` value.
|
||||||
|
Manually-added servers (without a tag) are preserved.
|
||||||
|
|
||||||
|
:param hub_name: Name of the hub whose servers should be removed.
|
||||||
|
:return: Number of servers removed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
fuzzforge_root = find_fuzzforge_root()
|
||||||
|
config_path = fuzzforge_root / "hub-config.json"
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = json.loads(config_path.read_text())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
existing = config.get("servers", [])
|
||||||
|
before = len(existing)
|
||||||
|
config["servers"] = [s for s in existing if s.get("source_hub") != hub_name]
|
||||||
|
after = len(config["servers"])
|
||||||
|
|
||||||
|
config_path.write_text(json.dumps(config, indent=2))
|
||||||
|
return before - after
|
||||||
1
fuzzforge-cli/src/fuzzforge_cli/tui/screens/__init__.py
Normal file
1
fuzzforge-cli/src/fuzzforge_cli/tui/screens/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""TUI screens for FuzzForge."""
|
||||||
96
fuzzforge-cli/src/fuzzforge_cli/tui/screens/agent_setup.py
Normal file
96
fuzzforge-cli/src/fuzzforge_cli/tui/screens/agent_setup.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""Agent setup and unlink modal screens for FuzzForge TUI.
|
||||||
|
|
||||||
|
Provides context-aware modals that receive the target agent directly
|
||||||
|
from the dashboard row selection — no redundant agent picker needed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.containers import Horizontal, Vertical
|
||||||
|
from textual.screen import ModalScreen
|
||||||
|
from textual.widgets import Button, Label, RadioButton, RadioSet
|
||||||
|
|
||||||
|
from fuzzforge_cli.commands.mcp import AIAgent
|
||||||
|
from fuzzforge_cli.tui.helpers import install_agent_config, uninstall_agent_config
|
||||||
|
|
||||||
|
|
||||||
|
class AgentSetupScreen(ModalScreen[str | None]):
|
||||||
|
"""Modal for linking a specific agent — only asks for engine choice."""
|
||||||
|
|
||||||
|
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||||
|
|
||||||
|
def __init__(self, agent: AIAgent, display_name: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._agent = agent
|
||||||
|
self._display_name = display_name
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the setup dialog layout."""
|
||||||
|
with Vertical(id="setup-dialog"):
|
||||||
|
yield Label(f"Setup {self._display_name}", classes="dialog-title")
|
||||||
|
|
||||||
|
yield Label("Container Engine:", classes="field-label")
|
||||||
|
yield RadioSet(
|
||||||
|
RadioButton("Docker", value=True),
|
||||||
|
RadioButton("Podman"),
|
||||||
|
id="engine-select",
|
||||||
|
)
|
||||||
|
|
||||||
|
with Horizontal(classes="dialog-buttons"):
|
||||||
|
yield Button("Install", variant="primary", id="btn-install")
|
||||||
|
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button clicks."""
|
||||||
|
if event.button.id == "btn-cancel":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn-install":
|
||||||
|
self._do_install()
|
||||||
|
|
||||||
|
def action_cancel(self) -> None:
|
||||||
|
"""Dismiss the dialog without action."""
|
||||||
|
self.dismiss(None)
|
||||||
|
|
||||||
|
def _do_install(self) -> None:
|
||||||
|
"""Execute the installation."""
|
||||||
|
engine_set = self.query_one("#engine-select", RadioSet)
|
||||||
|
engine = "docker" if engine_set.pressed_index <= 0 else "podman"
|
||||||
|
result = install_agent_config(self._agent, engine, force=True)
|
||||||
|
self.dismiss(result)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentUnlinkScreen(ModalScreen[str | None]):
|
||||||
|
"""Confirmation modal for unlinking a specific agent."""
|
||||||
|
|
||||||
|
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||||
|
|
||||||
|
def __init__(self, agent: AIAgent, display_name: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._agent = agent
|
||||||
|
self._display_name = display_name
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the unlink confirmation layout."""
|
||||||
|
with Vertical(id="unlink-dialog"):
|
||||||
|
yield Label(f"Unlink {self._display_name}?", classes="dialog-title")
|
||||||
|
yield Label(
|
||||||
|
f"This will remove the FuzzForge MCP configuration from {self._display_name}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
with Horizontal(classes="dialog-buttons"):
|
||||||
|
yield Button("Unlink", variant="warning", id="btn-unlink")
|
||||||
|
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button clicks."""
|
||||||
|
if event.button.id == "btn-cancel":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn-unlink":
|
||||||
|
result = uninstall_agent_config(self._agent)
|
||||||
|
self.dismiss(result)
|
||||||
|
|
||||||
|
def action_cancel(self) -> None:
|
||||||
|
"""Dismiss without action."""
|
||||||
|
self.dismiss(None)
|
||||||
299
fuzzforge-cli/src/fuzzforge_cli/tui/screens/hub_manager.py
Normal file
299
fuzzforge-cli/src/fuzzforge_cli/tui/screens/hub_manager.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
"""Hub management screens for FuzzForge TUI.
|
||||||
|
|
||||||
|
Provides modal dialogs for managing linked MCP hub repositories:
|
||||||
|
- HubManagerScreen: list, add, remove linked hubs
|
||||||
|
- LinkHubScreen: link a local directory as a hub
|
||||||
|
- CloneHubScreen: clone a git repo and link it (defaults to FuzzingLabs hub)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from rich.text import Text
|
||||||
|
from textual import work
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.containers import Horizontal, Vertical
|
||||||
|
from textual.screen import ModalScreen
|
||||||
|
from textual.widgets import Button, DataTable, Input, Label, Static
|
||||||
|
|
||||||
|
from fuzzforge_cli.tui.helpers import (
|
||||||
|
FUZZFORGE_DEFAULT_HUB_NAME,
|
||||||
|
FUZZFORGE_DEFAULT_HUB_URL,
|
||||||
|
clone_hub,
|
||||||
|
link_hub,
|
||||||
|
load_hubs_registry,
|
||||||
|
scan_hub_for_servers,
|
||||||
|
unlink_hub,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HubManagerScreen(ModalScreen[str | None]):
|
||||||
|
"""Modal screen for managing linked MCP hubs."""
|
||||||
|
|
||||||
|
BINDINGS = [("escape", "cancel", "Close")]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the hub manager layout."""
|
||||||
|
with Vertical(id="hub-manager-dialog"):
|
||||||
|
yield Label("Hub Manager", classes="dialog-title")
|
||||||
|
yield DataTable(id="hubs-table")
|
||||||
|
yield Label("", id="hub-status")
|
||||||
|
with Horizontal(classes="dialog-buttons"):
|
||||||
|
yield Button(
|
||||||
|
"FuzzingLabs Hub",
|
||||||
|
variant="primary",
|
||||||
|
id="btn-clone-default",
|
||||||
|
)
|
||||||
|
yield Button("Link Path", variant="default", id="btn-link")
|
||||||
|
yield Button("Clone URL", variant="default", id="btn-clone")
|
||||||
|
yield Button("Remove", variant="primary", id="btn-remove")
|
||||||
|
yield Button("Close", variant="default", id="btn-close")
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Populate the hubs table on startup."""
|
||||||
|
self._refresh_hubs()
|
||||||
|
|
||||||
|
def _refresh_hubs(self) -> None:
|
||||||
|
"""Refresh the linked hubs table."""
|
||||||
|
table = self.query_one("#hubs-table", DataTable)
|
||||||
|
table.clear(columns=True)
|
||||||
|
table.add_columns("Name", "Path", "Servers", "Source")
|
||||||
|
table.cursor_type = "row"
|
||||||
|
|
||||||
|
registry = load_hubs_registry()
|
||||||
|
hubs = registry.get("hubs", [])
|
||||||
|
|
||||||
|
if not hubs:
|
||||||
|
table.add_row(
|
||||||
|
Text("No hubs linked", style="dim"),
|
||||||
|
Text("Press 'FuzzingLabs Hub' to get started", style="dim"),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for hub in hubs:
|
||||||
|
name = hub.get("name", "unknown")
|
||||||
|
path = hub.get("path", "")
|
||||||
|
git_url = hub.get("git_url", "")
|
||||||
|
is_default = hub.get("is_default", False)
|
||||||
|
|
||||||
|
hub_path = Path(path)
|
||||||
|
if hub_path.is_dir():
|
||||||
|
servers = scan_hub_for_servers(hub_path)
|
||||||
|
count = str(len(servers))
|
||||||
|
else:
|
||||||
|
count = Text("dir missing", style="yellow")
|
||||||
|
|
||||||
|
source = git_url or "local"
|
||||||
|
if is_default:
|
||||||
|
name_cell = Text(f"★ {name}", style="bold")
|
||||||
|
else:
|
||||||
|
name_cell = Text(name)
|
||||||
|
|
||||||
|
table.add_row(name_cell, path, count, source)
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Route button actions."""
|
||||||
|
if event.button.id == "btn-close":
|
||||||
|
self.dismiss("refreshed")
|
||||||
|
elif event.button.id == "btn-clone-default":
|
||||||
|
self.app.push_screen(
|
||||||
|
CloneHubScreen(
|
||||||
|
FUZZFORGE_DEFAULT_HUB_URL,
|
||||||
|
FUZZFORGE_DEFAULT_HUB_NAME,
|
||||||
|
is_default=True,
|
||||||
|
),
|
||||||
|
callback=self._on_hub_action,
|
||||||
|
)
|
||||||
|
elif event.button.id == "btn-link":
|
||||||
|
self.app.push_screen(
|
||||||
|
LinkHubScreen(),
|
||||||
|
callback=self._on_hub_action,
|
||||||
|
)
|
||||||
|
elif event.button.id == "btn-clone":
|
||||||
|
self.app.push_screen(
|
||||||
|
CloneHubScreen(),
|
||||||
|
callback=self._on_hub_action,
|
||||||
|
)
|
||||||
|
elif event.button.id == "btn-remove":
|
||||||
|
self._remove_selected()
|
||||||
|
|
||||||
|
def _on_hub_action(self, result: str | None) -> None:
|
||||||
|
"""Handle result from a sub-screen."""
|
||||||
|
if result:
|
||||||
|
self.query_one("#hub-status", Label).update(result)
|
||||||
|
self.app.notify(result)
|
||||||
|
self._refresh_hubs()
|
||||||
|
|
||||||
|
def _remove_selected(self) -> None:
|
||||||
|
"""Remove the currently selected hub."""
|
||||||
|
table = self.query_one("#hubs-table", DataTable)
|
||||||
|
registry = load_hubs_registry()
|
||||||
|
hubs = registry.get("hubs", [])
|
||||||
|
|
||||||
|
if not hubs:
|
||||||
|
self.app.notify("No hubs to remove", severity="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
idx = table.cursor_row
|
||||||
|
if idx is None or idx < 0 or idx >= len(hubs):
|
||||||
|
self.app.notify("Select a hub to remove", severity="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
name = hubs[idx].get("name", "")
|
||||||
|
result = unlink_hub(name)
|
||||||
|
self.query_one("#hub-status", Label).update(result)
|
||||||
|
self._refresh_hubs()
|
||||||
|
self.app.notify(result)
|
||||||
|
|
||||||
|
def action_cancel(self) -> None:
|
||||||
|
"""Close the hub manager."""
|
||||||
|
self.dismiss("refreshed")
|
||||||
|
|
||||||
|
|
||||||
|
class LinkHubScreen(ModalScreen[str | None]):
|
||||||
|
"""Modal for linking a local directory as an MCP hub."""
|
||||||
|
|
||||||
|
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the link dialog layout."""
|
||||||
|
with Vertical(id="link-dialog"):
|
||||||
|
yield Label("Link Local Hub", classes="dialog-title")
|
||||||
|
|
||||||
|
yield Label("Hub Name:", classes="field-label")
|
||||||
|
yield Input(placeholder="my-hub", id="name-input")
|
||||||
|
|
||||||
|
yield Label("Directory Path:", classes="field-label")
|
||||||
|
yield Input(placeholder="/path/to/hub-directory", id="path-input")
|
||||||
|
|
||||||
|
yield Label("", id="link-status")
|
||||||
|
with Horizontal(classes="dialog-buttons"):
|
||||||
|
yield Button("Link", variant="primary", id="btn-link")
|
||||||
|
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button clicks."""
|
||||||
|
if event.button.id == "btn-cancel":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn-link":
|
||||||
|
self._do_link()
|
||||||
|
|
||||||
|
def _do_link(self) -> None:
|
||||||
|
"""Execute the link operation."""
|
||||||
|
name = self.query_one("#name-input", Input).value.strip()
|
||||||
|
path = self.query_one("#path-input", Input).value.strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
self.app.notify("Please enter a hub name", severity="warning")
|
||||||
|
return
|
||||||
|
if not path:
|
||||||
|
self.app.notify("Please enter a directory path", severity="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = link_hub(name, path)
|
||||||
|
self.dismiss(result)
|
||||||
|
|
||||||
|
def action_cancel(self) -> None:
|
||||||
|
"""Dismiss without action."""
|
||||||
|
self.dismiss(None)
|
||||||
|
|
||||||
|
|
||||||
|
class CloneHubScreen(ModalScreen[str | None]):
|
||||||
|
"""Modal for cloning a git hub repository and linking it.
|
||||||
|
|
||||||
|
When instantiated with *is_default=True* and FuzzingLabs URL,
|
||||||
|
provides a one-click setup for the standard security hub.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
default_url: str = "",
|
||||||
|
default_name: str = "",
|
||||||
|
is_default: bool = False,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._default_url = default_url
|
||||||
|
self._default_name = default_name
|
||||||
|
self._is_default = is_default
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Compose the clone dialog layout."""
|
||||||
|
title = "Clone FuzzingLabs Hub" if self._is_default else "Clone Git Hub"
|
||||||
|
with Vertical(id="clone-dialog"):
|
||||||
|
yield Label(title, classes="dialog-title")
|
||||||
|
|
||||||
|
yield Label("Git URL:", classes="field-label")
|
||||||
|
yield Input(
|
||||||
|
value=self._default_url,
|
||||||
|
placeholder="git@github.com:org/repo.git",
|
||||||
|
id="url-input",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield Label("Hub Name (optional):", classes="field-label")
|
||||||
|
yield Input(
|
||||||
|
value=self._default_name,
|
||||||
|
placeholder="auto-detect from URL",
|
||||||
|
id="name-input",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield Static("", id="clone-status")
|
||||||
|
with Horizontal(classes="dialog-buttons"):
|
||||||
|
yield Button(
|
||||||
|
"Clone & Link",
|
||||||
|
variant="primary",
|
||||||
|
id="btn-clone",
|
||||||
|
)
|
||||||
|
yield Button("Cancel", variant="default", id="btn-cancel")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button clicks."""
|
||||||
|
if event.button.id == "btn-cancel":
|
||||||
|
self.dismiss(None)
|
||||||
|
elif event.button.id == "btn-clone":
|
||||||
|
self._start_clone()
|
||||||
|
|
||||||
|
def _start_clone(self) -> None:
|
||||||
|
"""Validate input and start the async clone operation."""
|
||||||
|
url = self.query_one("#url-input", Input).value.strip()
|
||||||
|
if not url:
|
||||||
|
self.app.notify("Please enter a git URL", severity="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.query_one("#btn-clone", Button).disabled = True
|
||||||
|
self.query_one("#clone-status", Static).update("⏳ Cloning repository...")
|
||||||
|
self._do_clone(url)
|
||||||
|
|
||||||
|
@work(thread=True)
|
||||||
|
def _do_clone(self, url: str) -> None:
|
||||||
|
"""Clone the repo in a background thread."""
|
||||||
|
name_input = self.query_one("#name-input", Input).value.strip()
|
||||||
|
name = name_input or None
|
||||||
|
|
||||||
|
success, msg, path = clone_hub(url, name=name)
|
||||||
|
if success and path:
|
||||||
|
hub_name = name or path.name
|
||||||
|
link_result = link_hub(
|
||||||
|
hub_name,
|
||||||
|
path,
|
||||||
|
git_url=url,
|
||||||
|
is_default=self._is_default,
|
||||||
|
)
|
||||||
|
self.app.call_from_thread(self.dismiss, f"✓ {link_result}")
|
||||||
|
else:
|
||||||
|
self.app.call_from_thread(self._on_clone_failed, msg)
|
||||||
|
|
||||||
|
def _on_clone_failed(self, msg: str) -> None:
|
||||||
|
"""Handle a failed clone — re-enable the button and show the error."""
|
||||||
|
self.query_one("#clone-status", Static).update(f"✗ {msg}")
|
||||||
|
self.query_one("#btn-clone", Button).disabled = False
|
||||||
|
|
||||||
|
def action_cancel(self) -> None:
|
||||||
|
"""Dismiss without action."""
|
||||||
|
self.dismiss(None)
|
||||||
419
hub-config.json
419
hub-config.json
@@ -6,7 +6,9 @@
|
|||||||
"type": "docker",
|
"type": "docker",
|
||||||
"image": "nmap-mcp:latest",
|
"image": "nmap-mcp:latest",
|
||||||
"category": "reconnaissance",
|
"category": "reconnaissance",
|
||||||
"capabilities": ["NET_RAW"],
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -16,7 +18,9 @@
|
|||||||
"image": "binwalk-mcp:latest",
|
"image": "binwalk-mcp:latest",
|
||||||
"category": "binary-analysis",
|
"category": "binary-analysis",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -26,7 +30,9 @@
|
|||||||
"image": "yara-mcp:latest",
|
"image": "yara-mcp:latest",
|
||||||
"category": "binary-analysis",
|
"category": "binary-analysis",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -36,7 +42,9 @@
|
|||||||
"image": "capa-mcp:latest",
|
"image": "capa-mcp:latest",
|
||||||
"category": "binary-analysis",
|
"category": "binary-analysis",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,7 +54,9 @@
|
|||||||
"image": "radare2-mcp:latest",
|
"image": "radare2-mcp:latest",
|
||||||
"category": "binary-analysis",
|
"category": "binary-analysis",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -56,7 +66,9 @@
|
|||||||
"image": "ghcr.io/clearbluejar/pyghidra-mcp:latest",
|
"image": "ghcr.io/clearbluejar/pyghidra-mcp:latest",
|
||||||
"category": "binary-analysis",
|
"category": "binary-analysis",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -66,7 +78,9 @@
|
|||||||
"image": "searchsploit-mcp:latest",
|
"image": "searchsploit-mcp:latest",
|
||||||
"category": "exploitation",
|
"category": "exploitation",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -75,8 +89,12 @@
|
|||||||
"type": "docker",
|
"type": "docker",
|
||||||
"image": "nuclei-mcp:latest",
|
"image": "nuclei-mcp:latest",
|
||||||
"category": "web-security",
|
"category": "web-security",
|
||||||
"capabilities": ["NET_RAW"],
|
"capabilities": [
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,7 +104,9 @@
|
|||||||
"image": "trivy-mcp:latest",
|
"image": "trivy-mcp:latest",
|
||||||
"category": "cloud-security",
|
"category": "cloud-security",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -96,8 +116,385 @@
|
|||||||
"image": "gitleaks-mcp:latest",
|
"image": "gitleaks-mcp:latest",
|
||||||
"category": "secrets",
|
"category": "secrets",
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
"volumes": [
|
||||||
|
"~/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bloodhound-mcp",
|
||||||
|
"description": "bloodhound-mcp \u2014 active-directory",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "bloodhound-mcp:latest",
|
||||||
|
"category": "active-directory",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ida-mcp",
|
||||||
|
"description": "ida-mcp \u2014 binary-analysis",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "ida-mcp:latest",
|
||||||
|
"category": "binary-analysis",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "daml-viewer-mcp",
|
||||||
|
"description": "daml-viewer-mcp \u2014 blockchain",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "daml-viewer-mcp:latest",
|
||||||
|
"category": "blockchain",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "medusa-mcp",
|
||||||
|
"description": "medusa-mcp \u2014 blockchain",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "medusa-mcp:latest",
|
||||||
|
"category": "blockchain",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "solazy-mcp",
|
||||||
|
"description": "solazy-mcp \u2014 blockchain",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "solazy-mcp:latest",
|
||||||
|
"category": "blockchain",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prowler-mcp",
|
||||||
|
"description": "prowler-mcp \u2014 cloud-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "prowler-mcp:latest",
|
||||||
|
"category": "cloud-security",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "roadrecon-mcp",
|
||||||
|
"description": "roadrecon-mcp \u2014 cloud-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "roadrecon-mcp:latest",
|
||||||
|
"category": "cloud-security",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "semgrep-mcp",
|
||||||
|
"description": "semgrep-mcp \u2014 code-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "semgrep-mcp:latest",
|
||||||
|
"category": "code-security",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "boofuzz-mcp",
|
||||||
|
"description": "boofuzz-mcp \u2014 fuzzing",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "boofuzz-mcp:latest",
|
||||||
|
"category": "fuzzing",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dharma-mcp",
|
||||||
|
"description": "dharma-mcp \u2014 fuzzing",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "dharma-mcp:latest",
|
||||||
|
"category": "fuzzing",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dnstwist-mcp",
|
||||||
|
"description": "dnstwist-mcp \u2014 osint",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "dnstwist-mcp:latest",
|
||||||
|
"category": "osint",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maigret-mcp",
|
||||||
|
"description": "maigret-mcp \u2014 osint",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "maigret-mcp:latest",
|
||||||
|
"category": "osint",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hashcat-mcp",
|
||||||
|
"description": "hashcat-mcp \u2014 password-cracking",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "hashcat-mcp:latest",
|
||||||
|
"category": "password-cracking",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "externalattacker-mcp",
|
||||||
|
"description": "externalattacker-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "externalattacker-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "masscan-mcp",
|
||||||
|
"description": "masscan-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "masscan-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "networksdb-mcp",
|
||||||
|
"description": "networksdb-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "networksdb-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "pd-tools-mcp",
|
||||||
|
"description": "pd-tools-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "pd-tools-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shodan-mcp",
|
||||||
|
"description": "shodan-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "shodan-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "whatweb-mcp",
|
||||||
|
"description": "whatweb-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "whatweb-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zoomeye-mcp",
|
||||||
|
"description": "zoomeye-mcp \u2014 reconnaissance",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "zoomeye-mcp:latest",
|
||||||
|
"category": "reconnaissance",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "otx-mcp",
|
||||||
|
"description": "otx-mcp \u2014 threat-intel",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "otx-mcp:latest",
|
||||||
|
"category": "threat-intel",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "virustotal-mcp",
|
||||||
|
"description": "virustotal-mcp \u2014 threat-intel",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "virustotal-mcp:latest",
|
||||||
|
"category": "threat-intel",
|
||||||
|
"capabilities": [],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "burp-mcp",
|
||||||
|
"description": "burp-mcp \u2014 web-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "burp-mcp:latest",
|
||||||
|
"category": "web-security",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ffuf-mcp",
|
||||||
|
"description": "ffuf-mcp \u2014 web-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "ffuf-mcp:latest",
|
||||||
|
"category": "web-security",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nikto-mcp",
|
||||||
|
"description": "nikto-mcp \u2014 web-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "nikto-mcp:latest",
|
||||||
|
"category": "web-security",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sqlmap-mcp",
|
||||||
|
"description": "sqlmap-mcp \u2014 web-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "sqlmap-mcp:latest",
|
||||||
|
"category": "web-security",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "waybackurls-mcp",
|
||||||
|
"description": "waybackurls-mcp \u2014 web-security",
|
||||||
|
"type": "docker",
|
||||||
|
"image": "waybackurls-mcp:latest",
|
||||||
|
"category": "web-security",
|
||||||
|
"capabilities": [
|
||||||
|
"NET_RAW"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"/home/afredefon/FuzzingLabs/FuzzForge/fuzzforge-oss/.fuzzforge/hub/workspace:/data"
|
||||||
|
],
|
||||||
|
"enabled": true,
|
||||||
|
"source_hub": "mcp-security-hub"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default_timeout": 300,
|
"default_timeout": 300,
|
||||||
|
|||||||
Reference in New Issue
Block a user