mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-04-10 10:22:05 +02:00
Compare commits
38 Commits
refactor/r
...
feat/renam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be009a4094 | ||
|
|
bbf864e88b | ||
|
|
d04797b21d | ||
|
|
0ea8c4bd1d | ||
|
|
af7532c811 | ||
|
|
0d410bd5b4 | ||
|
|
d3a20b3846 | ||
|
|
664278da3f | ||
|
|
9374fd3aee | ||
|
|
01e6bc3fb1 | ||
|
|
b634214e01 | ||
|
|
e7022c2c82 | ||
|
|
2e96517d11 | ||
|
|
575b90f8d4 | ||
|
|
c59b6ba81a | ||
|
|
a51c495d34 | ||
|
|
7924e44245 | ||
|
|
a824809294 | ||
|
|
07c32de294 | ||
|
|
bc5e9373ce | ||
|
|
73a0170d65 | ||
|
|
6cdd0caec0 | ||
|
|
462f6ed408 | ||
|
|
9cfbc29677 | ||
|
|
6ced81affc | ||
|
|
b975d285c6 | ||
|
|
1891a43189 | ||
|
|
a3441676a3 | ||
|
|
f192771b9b | ||
|
|
976947cf5c | ||
|
|
544569ddbd | ||
|
|
6f967fff63 | ||
|
|
47c254e2bd | ||
|
|
b137f48e7f | ||
|
|
f8002254e5 | ||
|
|
f2dca0a7e7 | ||
|
|
9376645197 | ||
|
|
3e0d1cd02f |
86
.github/workflows/ci.yml
vendored
Normal file
86
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev, feature/*]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint-and-typecheck:
|
||||
name: Lint & Type Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Ruff check (secpipe-cli)
|
||||
run: |
|
||||
cd secpipe-cli
|
||||
uv run --extra lints ruff check src/
|
||||
|
||||
- name: Ruff check (secpipe-mcp)
|
||||
run: |
|
||||
cd secpipe-mcp
|
||||
uv run --extra lints ruff check src/
|
||||
|
||||
- name: Ruff check (secpipe-common)
|
||||
run: |
|
||||
cd secpipe-common
|
||||
uv run --extra lints ruff check src/
|
||||
|
||||
- name: Mypy type check (secpipe-cli)
|
||||
run: |
|
||||
cd secpipe-cli
|
||||
uv run --extra lints mypy src/
|
||||
|
||||
- name: Mypy type check (secpipe-mcp)
|
||||
run: |
|
||||
cd secpipe-mcp
|
||||
uv run --extra lints mypy src/
|
||||
|
||||
# NOTE: Mypy check for secpipe-common temporarily disabled
|
||||
# due to 37 pre-existing type errors in legacy code.
|
||||
# TODO: Fix type errors and re-enable strict checking
|
||||
#- name: Mypy type check (secpipe-common)
|
||||
# run: |
|
||||
# cd secpipe-common
|
||||
# uv run --extra lints mypy src/
|
||||
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras
|
||||
|
||||
- name: Run MCP tests
|
||||
run: |
|
||||
cd secpipe-mcp
|
||||
uv run --extra tests pytest -v
|
||||
|
||||
- name: Run common tests
|
||||
run: |
|
||||
cd secpipe-common
|
||||
uv run --extra tests pytest -v
|
||||
49
.github/workflows/mcp-server.yml
vendored
Normal file
49
.github/workflows/mcp-server.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: MCP Server Smoke Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
mcp-server:
|
||||
name: MCP Server Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --all-extras
|
||||
|
||||
- name: Start MCP server in background
|
||||
run: |
|
||||
cd secpipe-mcp
|
||||
nohup uv run uvicorn secpipe_mcp.application:app --host 127.0.0.1 --port 8000 > server.log 2>&1 &
|
||||
echo $! > server.pid
|
||||
sleep 3
|
||||
|
||||
- name: Run MCP tool tests
|
||||
run: |
|
||||
cd secpipe-mcp
|
||||
uv run --extra tests pytest tests/test_resources.py -v
|
||||
|
||||
- name: Stop MCP server
|
||||
if: always()
|
||||
run: |
|
||||
if [ -f secpipe-mcp/server.pid ]; then
|
||||
kill $(cat secpipe-mcp/server.pid) || true
|
||||
fi
|
||||
|
||||
- name: Show server logs
|
||||
if: failure()
|
||||
run: cat secpipe-mcp/server.log || true
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,3 +10,7 @@ __pycache__
|
||||
|
||||
# Podman/Docker container storage artifacts
|
||||
~/.fuzzforge/
|
||||
|
||||
# User-specific hub config (generated at runtime)
|
||||
hub-config.json
|
||||
*.egg-info/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Contributing to FuzzForge AI
|
||||
# Contributing to SecPipe AI
|
||||
|
||||
Thank you for your interest in contributing to FuzzForge AI! We welcome contributions from the community and are excited to collaborate with you.
|
||||
Thank you for your interest in contributing to SecPipe AI! We welcome contributions from the community and are excited to collaborate with you.
|
||||
|
||||
**Our Vision**: FuzzForge aims to be a **universal platform for security research** across all cybersecurity domains. Through our modular architecture, any security tool—from fuzzing engines to cloud scanners, from mobile app analyzers to IoT security tools—can be integrated as a containerized module and controlled via AI agents.
|
||||
**Our Vision**: SecPipe aims to be a **universal platform for security research** across all cybersecurity domains. Through our modular architecture, any security tool—from fuzzing engines to cloud scanners, from mobile app analyzers to IoT security tools—can be integrated as a containerized module and controlled via AI agents.
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
@@ -13,7 +13,7 @@ Thank you for your interest in contributing to FuzzForge AI! We welcome contribu
|
||||
- **Documentation** - Improve guides, tutorials, and module documentation
|
||||
- **Testing** - Help test new features and report issues
|
||||
- **AI Integration** - Improve MCP tools and AI agent interactions
|
||||
- **Tool Integrations** - Wrap existing security tools as FuzzForge modules
|
||||
- **Tool Integrations** - Wrap existing security tools as SecPipe modules
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
@@ -71,13 +71,13 @@ test(runner): add container execution tests
|
||||
3. **Test Your Changes**
|
||||
```bash
|
||||
# Test modules
|
||||
FUZZFORGE_MODULES_PATH=./fuzzforge-modules uv run fuzzforge modules list
|
||||
SECPIPE_MODULES_PATH=./secpipe-modules uv run secpipe modules list
|
||||
|
||||
# Run a module
|
||||
uv run fuzzforge modules run your-module --assets ./test-assets
|
||||
uv run secpipe modules run your-module --assets ./test-assets
|
||||
|
||||
# Test MCP integration (if applicable)
|
||||
uv run fuzzforge mcp status
|
||||
uv run secpipe mcp status
|
||||
```
|
||||
|
||||
4. **Submit Pull Request**
|
||||
@@ -88,11 +88,11 @@ test(runner): add container execution tests
|
||||
|
||||
## Module Development
|
||||
|
||||
FuzzForge uses a modular architecture where security tools run as isolated containers. The `fuzzforge-modules-sdk` provides everything you need to create new modules.
|
||||
SecPipe uses a modular architecture where security tools run as isolated containers. The `secpipe-modules-sdk` provides everything you need to create new modules.
|
||||
|
||||
**Documentation:**
|
||||
- [Module SDK Documentation](fuzzforge-modules/fuzzforge-modules-sdk/README.md) - Complete SDK reference
|
||||
- [Module Template](fuzzforge-modules/fuzzforge-module-template/) - Starting point for new modules
|
||||
- [Module SDK Documentation](secpipe-modules/secpipe-modules-sdk/README.md) - Complete SDK reference
|
||||
- [Module Template](secpipe-modules/secpipe-module-template/) - Starting point for new modules
|
||||
- [USAGE Guide](USAGE.md) - Setup and installation instructions
|
||||
|
||||
### Creating a New Module
|
||||
@@ -100,8 +100,8 @@ FuzzForge uses a modular architecture where security tools run as isolated conta
|
||||
1. **Use the Module Template**
|
||||
```bash
|
||||
# Generate a new module from template
|
||||
cd fuzzforge-modules/
|
||||
cp -r fuzzforge-module-template my-new-module
|
||||
cd secpipe-modules/
|
||||
cp -r secpipe-module-template my-new-module
|
||||
cd my-new-module
|
||||
```
|
||||
|
||||
@@ -127,8 +127,8 @@ FuzzForge uses a modular architecture where security tools run as isolated conta
|
||||
|
||||
Edit `src/module/mod.py`:
|
||||
```python
|
||||
from fuzzforge_modules_sdk.api.modules import BaseModule
|
||||
from fuzzforge_modules_sdk.api.models import ModuleResult
|
||||
from secpipe_modules_sdk.api.modules import BaseModule
|
||||
from secpipe_modules_sdk.api.models import ModuleResult
|
||||
from .models import MyModuleConfig, MyModuleOutput
|
||||
|
||||
class MyModule(BaseModule[MyModuleConfig, MyModuleOutput]):
|
||||
@@ -157,7 +157,7 @@ FuzzForge uses a modular architecture where security tools run as isolated conta
|
||||
Edit `src/module/models.py`:
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from fuzzforge_modules_sdk.api.models import BaseModuleConfig, BaseModuleOutput
|
||||
from secpipe_modules_sdk.api.models import BaseModuleConfig, BaseModuleOutput
|
||||
|
||||
class MyModuleConfig(BaseModuleConfig):
|
||||
"""Configuration for your module."""
|
||||
@@ -173,31 +173,31 @@ FuzzForge uses a modular architecture where security tools run as isolated conta
|
||||
5. **Build Your Module**
|
||||
```bash
|
||||
# Build the SDK first (if not already done)
|
||||
cd ../fuzzforge-modules-sdk
|
||||
cd ../secpipe-modules-sdk
|
||||
uv build
|
||||
mkdir -p .wheels
|
||||
cp ../../dist/fuzzforge_modules_sdk-*.whl .wheels/
|
||||
cp ../../dist/secpipe_modules_sdk-*.whl .wheels/
|
||||
cd ../..
|
||||
docker build -t localhost/fuzzforge-modules-sdk:0.1.0 fuzzforge-modules/fuzzforge-modules-sdk/
|
||||
docker build -t localhost/secpipe-modules-sdk:0.1.0 secpipe-modules/secpipe-modules-sdk/
|
||||
|
||||
# Build your module
|
||||
cd fuzzforge-modules/my-new-module
|
||||
docker build -t fuzzforge-my-new-module:0.1.0 .
|
||||
cd secpipe-modules/my-new-module
|
||||
docker build -t secpipe-my-new-module:0.1.0 .
|
||||
```
|
||||
|
||||
6. **Test Your Module**
|
||||
```bash
|
||||
# Run with test assets
|
||||
uv run fuzzforge modules run my-new-module --assets ./test-assets
|
||||
uv run secpipe modules run my-new-module --assets ./test-assets
|
||||
|
||||
# Check module info
|
||||
uv run fuzzforge modules info my-new-module
|
||||
uv run secpipe modules info my-new-module
|
||||
```
|
||||
|
||||
### Module Development Guidelines
|
||||
|
||||
**Important Conventions:**
|
||||
- **Input/Output**: Use `/fuzzforge/input` for assets and `/fuzzforge/output` for results
|
||||
- **Input/Output**: Use `/secpipe/input` for assets and `/secpipe/output` for results
|
||||
- **Configuration**: Support JSON configuration via stdin or file
|
||||
- **Logging**: Use structured logging (structlog is pre-configured)
|
||||
- **Error Handling**: Return proper exit codes and error messages
|
||||
@@ -206,12 +206,12 @@ FuzzForge uses a modular architecture where security tools run as isolated conta
|
||||
- **Dependencies**: Minimize container size, use multi-stage builds
|
||||
|
||||
**See also:**
|
||||
- [Module SDK API Reference](fuzzforge-modules/fuzzforge-modules-sdk/src/fuzzforge_modules_sdk/api/)
|
||||
- [Module SDK API Reference](secpipe-modules/secpipe-modules-sdk/src/secpipe_modules_sdk/api/)
|
||||
- [Dockerfile Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
|
||||
|
||||
### Module Types
|
||||
|
||||
FuzzForge is designed to support modules across **all cybersecurity domains**. The modular architecture allows any security tool to be containerized and integrated. Here are the main categories:
|
||||
SecPipe is designed to support modules across **all cybersecurity domains**. The modular architecture allows any security tool to be containerized and integrated. Here are the main categories:
|
||||
|
||||
**Application Security**
|
||||
- Fuzzing engines (coverage-guided, grammar-based, mutation-based)
|
||||
@@ -273,8 +273,8 @@ FuzzForge is designed to support modules across **all cybersecurity domains**. T
|
||||
```python
|
||||
# src/module/mod.py
|
||||
from pathlib import Path
|
||||
from fuzzforge_modules_sdk.api.modules import BaseModule
|
||||
from fuzzforge_modules_sdk.api.models import ModuleResult
|
||||
from secpipe_modules_sdk.api.modules import BaseModule
|
||||
from secpipe_modules_sdk.api.models import ModuleResult
|
||||
from .models import ScannerConfig, ScannerOutput
|
||||
|
||||
class SecurityScanner(BaseModule[ScannerConfig, ScannerOutput]):
|
||||
@@ -341,7 +341,7 @@ uv run pytest
|
||||
|
||||
## Contributing to Core Features
|
||||
|
||||
Beyond modules, you can contribute to FuzzForge's core components.
|
||||
Beyond modules, you can contribute to SecPipe's core components.
|
||||
|
||||
**Useful Resources:**
|
||||
- [Project Structure](README.md) - Overview of the codebase
|
||||
@@ -350,18 +350,18 @@ Beyond modules, you can contribute to FuzzForge's core components.
|
||||
|
||||
### Core Components
|
||||
|
||||
- **fuzzforge-mcp** - MCP server for AI agent integration
|
||||
- **fuzzforge-runner** - Module execution engine
|
||||
- **fuzzforge-cli** - Command-line interface
|
||||
- **fuzzforge-common** - Shared utilities and sandbox engines
|
||||
- **fuzzforge-types** - Type definitions and schemas
|
||||
- **secpipe-mcp** - MCP server for AI agent integration
|
||||
- **secpipe-runner** - Module execution engine
|
||||
- **secpipe-cli** - Command-line interface
|
||||
- **secpipe-common** - Shared utilities and sandbox engines
|
||||
- **secpipe-types** - Type definitions and schemas
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. **Clone and Install**
|
||||
```bash
|
||||
git clone https://github.com/FuzzingLabs/fuzzforge_ai.git
|
||||
cd fuzzforge_ai
|
||||
git clone https://github.com/FuzzingLabs/secpipe_ai.git
|
||||
cd secpipe_ai
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
@@ -371,7 +371,7 @@ Beyond modules, you can contribute to FuzzForge's core components.
|
||||
make test
|
||||
|
||||
# Run specific package tests
|
||||
cd fuzzforge-mcp
|
||||
cd secpipe-mcp
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
@@ -381,7 +381,7 @@ Beyond modules, you can contribute to FuzzForge's core components.
|
||||
make typecheck
|
||||
|
||||
# Type check specific package
|
||||
cd fuzzforge-runner
|
||||
cd secpipe-runner
|
||||
uv run mypy .
|
||||
```
|
||||
|
||||
@@ -399,7 +399,7 @@ Beyond modules, you can contribute to FuzzForge's core components.
|
||||
When reporting bugs, please include:
|
||||
|
||||
- **Environment**: OS, Python version, Docker version, uv version
|
||||
- **FuzzForge Version**: Output of `uv run fuzzforge --version`
|
||||
- **SecPipe Version**: Output of `uv run secpipe --version`
|
||||
- **Module**: Which module or component is affected
|
||||
- **Steps to Reproduce**: Clear steps to recreate the issue
|
||||
- **Expected Behavior**: What should happen
|
||||
@@ -419,7 +419,7 @@ When reporting bugs, please include:
|
||||
**Module:** my-custom-scanner
|
||||
|
||||
**Steps to Reproduce:**
|
||||
1. Run `uv run fuzzforge modules run my-scanner --assets ./test-target`
|
||||
1. Run `uv run secpipe modules run my-scanner --assets ./test-target`
|
||||
2. Module fails with timeout error
|
||||
|
||||
**Expected:** Module completes analysis
|
||||
@@ -491,7 +491,7 @@ Brief description of what this module does.
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
uv run fuzzforge modules run module-name --assets ./path/to/assets
|
||||
uv run secpipe modules run module-name --assets ./path/to/assets
|
||||
\`\`\`
|
||||
|
||||
## Output
|
||||
@@ -538,7 +538,7 @@ Before submitting a new module:
|
||||
|
||||
## License
|
||||
|
||||
By contributing to FuzzForge AI, you agree that your contributions will be licensed under the same license as the project (see [LICENSE](LICENSE)).
|
||||
By contributing to SecPipe AI, you agree that your contributions will be licensed under the same license as the project (see [LICENSE](LICENSE)).
|
||||
|
||||
For module contributions:
|
||||
- Modules you create remain under the project license
|
||||
@@ -552,12 +552,12 @@ For module contributions:
|
||||
Need help contributing?
|
||||
|
||||
- Join our [Discord](https://discord.gg/8XEX33UUwZ)
|
||||
- Read the [Module SDK Documentation](fuzzforge-modules/fuzzforge-modules-sdk/README.md)
|
||||
- Read the [Module SDK Documentation](secpipe-modules/secpipe-modules-sdk/README.md)
|
||||
- Check the module template for examples
|
||||
- Contact: contact@fuzzinglabs.com
|
||||
|
||||
---
|
||||
|
||||
**Thank you for making FuzzForge better!**
|
||||
**Thank you for making SecPipe better!**
|
||||
|
||||
Every contribution, no matter how small, helps build a stronger security research platform. Whether you're creating a module for web security, cloud scanning, mobile analysis, or any other cybersecurity domain, your work makes FuzzForge more powerful and versatile for the entire security community!
|
||||
Every contribution, no matter how small, helps build a stronger security research platform. Whether you're creating a module for web security, cloud scanning, mobile analysis, or any other cybersecurity domain, your work makes SecPipe more powerful and versatile for the entire security community!
|
||||
|
||||
14
Makefile
14
Makefile
@@ -4,7 +4,7 @@ SHELL := /bin/bash
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "FuzzForge AI Development Commands"
|
||||
@echo "SecPipe AI Development Commands"
|
||||
@echo ""
|
||||
@echo " make install - Install all dependencies"
|
||||
@echo " make sync - Sync shared packages from upstream"
|
||||
@@ -20,17 +20,17 @@ help:
|
||||
install:
|
||||
uv sync
|
||||
|
||||
# Sync shared packages from upstream fuzzforge-core
|
||||
# Sync shared packages from upstream secpipe-core
|
||||
sync:
|
||||
@if [ -z "$(UPSTREAM)" ]; then \
|
||||
echo "Usage: make sync UPSTREAM=/path/to/fuzzforge-core"; \
|
||||
echo "Usage: make sync UPSTREAM=/path/to/secpipe-core"; \
|
||||
exit 1; \
|
||||
fi
|
||||
./scripts/sync-upstream.sh $(UPSTREAM)
|
||||
|
||||
# Format all packages
|
||||
format:
|
||||
@for pkg in packages/fuzzforge-*/; do \
|
||||
@for pkg in packages/secpipe-*/; do \
|
||||
if [ -f "$$pkg/pyproject.toml" ]; then \
|
||||
echo "Formatting $$pkg..."; \
|
||||
cd "$$pkg" && uv run ruff format . && cd -; \
|
||||
@@ -39,7 +39,7 @@ format:
|
||||
|
||||
# Lint all packages
|
||||
lint:
|
||||
@for pkg in packages/fuzzforge-*/; do \
|
||||
@for pkg in packages/secpipe-*/; do \
|
||||
if [ -f "$$pkg/pyproject.toml" ]; then \
|
||||
echo "Linting $$pkg..."; \
|
||||
cd "$$pkg" && uv run ruff check . && cd -; \
|
||||
@@ -48,7 +48,7 @@ lint:
|
||||
|
||||
# Type check all packages
|
||||
typecheck:
|
||||
@for pkg in packages/fuzzforge-*/; do \
|
||||
@for pkg in packages/secpipe-*/; do \
|
||||
if [ -f "$$pkg/pyproject.toml" ] && [ -f "$$pkg/mypy.ini" ]; then \
|
||||
echo "Type checking $$pkg..."; \
|
||||
cd "$$pkg" && uv run mypy . && cd -; \
|
||||
@@ -57,7 +57,7 @@ typecheck:
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
@for pkg in packages/fuzzforge-*/; do \
|
||||
@for pkg in packages/secpipe-*/; do \
|
||||
if [ -f "$$pkg/pytest.ini" ]; then \
|
||||
echo "Testing $$pkg..."; \
|
||||
cd "$$pkg" && uv run pytest && cd -; \
|
||||
|
||||
4
NOTICE
4
NOTICE
@@ -1,4 +1,4 @@
|
||||
FuzzForge
|
||||
SecPipe
|
||||
Copyright (c) 2025 FuzzingLabs
|
||||
|
||||
This product includes software developed by FuzzingLabs (https://fuzzforge.ai).
|
||||
@@ -7,6 +7,6 @@ Licensed under the Business Source License 1.1 (BSL).
|
||||
After the Change Date (four years from the date of publication), this version
|
||||
of the Licensed Work will be made available under the Apache License, Version 2.0.
|
||||
|
||||
You may not use the name "FuzzingLabs" or "FuzzForge" nor the names of its
|
||||
You may not use the name "FuzzingLabs" or "SecPipe" nor the names of its
|
||||
contributors to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
275
README.md
275
README.md
@@ -1,4 +1,4 @@
|
||||
<h1 align="center"> FuzzForge AI</h1>
|
||||
<h1 align="center">SecPipe</h1>
|
||||
<h3 align="center">AI-Powered Security Research Orchestration via MCP</h3>
|
||||
|
||||
<p align="center">
|
||||
@@ -6,7 +6,6 @@
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-BSL%201.1-blue" alt="License: BSL 1.1"></a>
|
||||
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python 3.12+"/></a>
|
||||
<a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-compatible-green" alt="MCP Compatible"/></a>
|
||||
<a href="https://fuzzforge.ai"><img src="https://img.shields.io/badge/Website-fuzzforge.ai-purple" alt="Website"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -17,63 +16,68 @@
|
||||
<sub>
|
||||
<a href="#-overview"><b>Overview</b></a> •
|
||||
<a href="#-features"><b>Features</b></a> •
|
||||
<a href="#-mcp-security-hub"><b>Security Hub</b></a> •
|
||||
<a href="#-installation"><b>Installation</b></a> •
|
||||
<a href="USAGE.md"><b>Usage Guide</b></a> •
|
||||
<a href="#-modules"><b>Modules</b></a> •
|
||||
<a href="#-contributing"><b>Contributing</b></a>
|
||||
</sub>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> 🚧 **FuzzForge AI is under active development.** Expect breaking changes and new features!
|
||||
> 🚧 **SecPipe AI is under active development.** Expect breaking changes and new features!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Overview
|
||||
|
||||
**FuzzForge AI** is an open-source runtime that enables AI agents (GitHub Copilot, Claude, etc.) to orchestrate security research workflows through the **Model Context Protocol (MCP)**.
|
||||
**SecPipe AI** is an open-source MCP server that enables AI agents (GitHub Copilot, Claude, etc.) to orchestrate security research workflows through the **Model Context Protocol (MCP)**.
|
||||
|
||||
### The Core: Modules
|
||||
SecPipe connects your AI assistant to **MCP tool hubs** — collections of containerized security tools that the agent can discover, chain, and execute autonomously. Instead of manually running security tools, describe what you want and let your AI assistant handle it.
|
||||
|
||||
At the heart of FuzzForge are **modules** - containerized security tools that AI agents can discover, configure, and orchestrate. Each module encapsulates a specific security capability (static analysis, fuzzing, crash analysis, etc.) and runs in an isolated container.
|
||||
### The Core: Hub Architecture
|
||||
|
||||
- **🔌 Plug & Play**: Modules are self-contained - just pull and run
|
||||
- **🤖 AI-Native**: Designed for AI agent orchestration via MCP
|
||||
- **🔗 Composable**: Chain modules together into automated workflows
|
||||
- **📦 Extensible**: Build custom modules with the Python SDK
|
||||
SecPipe acts as a **meta-MCP server** — a single MCP endpoint that gives your AI agent access to tools from multiple MCP hub servers. Each hub server is a containerized security tool (Binwalk, YARA, Radare2, Nmap, etc.) that the agent can discover at runtime.
|
||||
|
||||
FuzzForge AI handles module discovery, execution, and result collection. Security modules (developed separately) provide the actual security tooling - from static analyzers to fuzzers to crash triagers.
|
||||
- **🔍 Discovery**: The agent lists available hub servers and discovers their tools
|
||||
- **🤖 AI-Native**: Hub tools provide agent context — usage tips, workflow guidance, and domain knowledge
|
||||
- **🔗 Composable**: Chain tools from different hubs into automated pipelines
|
||||
- **📦 Extensible**: Add your own MCP servers to the hub registry
|
||||
|
||||
Instead of manually running security tools, describe what you want and let your AI assistant handle it.
|
||||
### 🎬 Use Case: Firmware Vulnerability Research
|
||||
|
||||
> **Scenario**: Analyze a firmware image to find security vulnerabilities — fully automated by an AI agent.
|
||||
|
||||
```
|
||||
User: "Search for vulnerabilities in firmware.bin"
|
||||
|
||||
Agent → Binwalk: Extract filesystem from firmware image
|
||||
Agent → YARA: Scan extracted files for vulnerability patterns
|
||||
Agent → Radare2: Trace dangerous function calls in prioritized binaries
|
||||
Agent → Report: 8 vulnerabilities found (2 critical, 4 high, 2 medium)
|
||||
```
|
||||
|
||||
### 🎬 Use Case: Rust Fuzzing Pipeline
|
||||
|
||||
> **Scenario**: Fuzz a Rust crate to discover vulnerabilities using AI-assisted harness generation and parallel fuzzing.
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<th>1️⃣ Analyze, Generate & Validate Harnesses</th>
|
||||
<th>2️⃣ Run Parallel Continuous Fuzzing</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="assets/demopart2.gif" alt="FuzzForge Demo - Analysis Pipeline" width="100%"></td>
|
||||
<td><img src="assets/demopart1.gif" alt="FuzzForge Demo - Parallel Fuzzing" width="100%"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><sub>AI agent analyzes code, generates harnesses, and validates they compile</sub></td>
|
||||
<td align="center"><sub>Multiple fuzzing sessions run in parallel with live metrics</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
```
|
||||
User: "Fuzz the blurhash crate for vulnerabilities"
|
||||
|
||||
Agent → Rust Analyzer: Identify fuzzable functions and attack surface
|
||||
Agent → Harness Gen: Generate and validate fuzzing harnesses
|
||||
Agent → Cargo Fuzzer: Run parallel coverage-guided fuzzing sessions
|
||||
Agent → Crash Analysis: Deduplicate and triage discovered crashes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Support the Project
|
||||
|
||||
If you find FuzzForge useful, please **star the repo** to support development! 🚀
|
||||
If you find SecPipe useful, please **star the repo** to support development! 🚀
|
||||
|
||||
<a href="https://github.com/FuzzingLabs/fuzzforge_ai/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/FuzzingLabs/fuzzforge_ai?style=social" alt="GitHub Stars">
|
||||
<a href="https://github.com/FuzzingLabs/secpipe_ai/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/FuzzingLabs/secpipe_ai?style=social" alt="GitHub Stars">
|
||||
</a>
|
||||
|
||||
---
|
||||
@@ -82,13 +86,13 @@ If you find FuzzForge useful, please **star the repo** to support development!
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🤖 **AI-Native** | Built for MCP - works with GitHub Copilot, Claude, and any MCP-compatible agent |
|
||||
| 📦 **Containerized** | Each module runs in isolation via Docker or Podman |
|
||||
| 🔄 **Continuous Mode** | Long-running tasks (fuzzing) with real-time metrics streaming |
|
||||
| 🔗 **Workflows** | Chain multiple modules together in automated pipelines |
|
||||
| 🛠️ **Extensible** | Create custom modules with the Python SDK |
|
||||
| 🏠 **Local First** | All execution happens on your machine - no cloud required |
|
||||
| 🔒 **Secure** | Sandboxed containers with no network access by default |
|
||||
| 🤖 **AI-Native** | Built for MCP — works with GitHub Copilot, Claude, and any MCP-compatible agent |
|
||||
| 🔌 **Hub System** | Connect to MCP tool hubs — each hub brings dozens of containerized security tools |
|
||||
| 🔍 **Tool Discovery** | Agents discover available tools at runtime with built-in usage guidance |
|
||||
| 🔗 **Pipelines** | Chain tools from different hubs into automated multi-step workflows |
|
||||
| 🔄 **Persistent Sessions** | Long-running tools (Radare2, fuzzers) with stateful container sessions |
|
||||
| 🏠 **Local First** | All execution happens on your machine — no cloud required |
|
||||
| 🔒 **Sandboxed** | Every tool runs in an isolated container via Docker or Podman |
|
||||
|
||||
---
|
||||
|
||||
@@ -101,28 +105,58 @@ If you find FuzzForge useful, please **star the repo** to support development!
|
||||
│ MCP Protocol (stdio)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FuzzForge MCP Server │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
|
||||
│ │list_modules │ │execute_module│ │start_continuous_module │ │
|
||||
│ └─────────────┘ └──────────────┘ └────────────────────────┘ │
|
||||
│ SecPipe MCP Server │
|
||||
│ │
|
||||
│ Projects Hub Discovery Hub Execution │
|
||||
│ ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ │
|
||||
│ │init_project │ │list_hub_servers │ │execute_hub_tool │ │
|
||||
│ │set_assets │ │discover_hub_tools│ │start_hub_server │ │
|
||||
│ │list_results │ │get_tool_schema │ │stop_hub_server │ │
|
||||
│ └──────────────┘ └──────────────────┘ └───────────────────┘ │
|
||||
└───────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
│ Docker/Podman
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FuzzForge Runner │
|
||||
│ Container Engine (Docker/Podman) │
|
||||
└───────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ Module A │ │ Module B │ │ Module C │
|
||||
│ (Container) │ │ (Container) │ │ (Container) │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
│ MCP Hub Servers │
|
||||
│ │
|
||||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ Binwalk │ │ YARA │ │ Radare2 │ │ Nmap │ │
|
||||
│ │ 6 tools │ │ 5 tools │ │ 32 tools │ │ 8 tools │ │
|
||||
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
|
||||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ Nuclei │ │ SQLMap │ │ Trivy │ │ ... │ │
|
||||
│ │ 7 tools │ │ 8 tools │ │ 7 tools │ │ 36 hubs │ │
|
||||
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 MCP Security Hub
|
||||
|
||||
SecPipe ships with built-in support for the **[MCP Security Hub](https://github.com/FuzzingLabs/mcp-security-hub)** — a collection of 36 production-ready, Dockerized MCP servers covering offensive security:
|
||||
|
||||
| Category | Servers | Examples |
|
||||
|----------|---------|----------|
|
||||
| 🔍 **Reconnaissance** | 8 | Nmap, Masscan, Shodan, WhatWeb |
|
||||
| 🌐 **Web Security** | 6 | Nuclei, SQLMap, ffuf, Nikto |
|
||||
| 🔬 **Binary Analysis** | 6 | Radare2, Binwalk, YARA, Capa, Ghidra |
|
||||
| ⛓️ **Blockchain** | 3 | Medusa, Solazy, DAML Viewer |
|
||||
| ☁️ **Cloud Security** | 3 | Trivy, Prowler, RoadRecon |
|
||||
| 💻 **Code Security** | 1 | Semgrep |
|
||||
| 🔑 **Secrets Detection** | 1 | Gitleaks |
|
||||
| 💥 **Exploitation** | 1 | SearchSploit |
|
||||
| 🎯 **Fuzzing** | 2 | Boofuzz, Dharma |
|
||||
| 🕵️ **OSINT** | 2 | Maigret, DNSTwist |
|
||||
| 🛡️ **Threat Intel** | 2 | VirusTotal, AlienVault OTX |
|
||||
| 🏰 **Active Directory** | 1 | BloodHound |
|
||||
|
||||
> 185+ individual tools accessible through a single MCP connection.
|
||||
|
||||
The hub is open source and can be extended with your own MCP servers. See the [mcp-security-hub repository](https://github.com/FuzzingLabs/mcp-security-hub) for details.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Prerequisites
|
||||
@@ -135,138 +169,77 @@ If you find FuzzForge useful, please **star the repo** to support development!
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/FuzzingLabs/fuzzforge_ai.git
|
||||
cd fuzzforge_ai
|
||||
git clone https://github.com/FuzzingLabs/secpipe_ai.git
|
||||
cd secpipe_ai
|
||||
|
||||
# Install dependencies
|
||||
uv sync
|
||||
|
||||
# Build module images
|
||||
make build-modules
|
||||
```
|
||||
|
||||
### Link the Security Hub
|
||||
|
||||
```bash
|
||||
# Clone the MCP Security Hub
|
||||
git clone https://github.com/FuzzingLabs/mcp-security-hub.git ~/.secpipe/hubs/mcp-security-hub
|
||||
|
||||
# Build the Docker images for the hub tools
|
||||
./scripts/build-hub-images.sh
|
||||
```
|
||||
|
||||
Or use the terminal UI (`uv run secpipe ui`) to link hubs interactively.
|
||||
|
||||
### Configure MCP for Your AI Agent
|
||||
|
||||
```bash
|
||||
# For GitHub Copilot
|
||||
uv run fuzzforge mcp install copilot
|
||||
uv run secpipe mcp install copilot
|
||||
|
||||
# For Claude Code (CLI)
|
||||
uv run fuzzforge mcp install claude-code
|
||||
uv run secpipe mcp install claude-code
|
||||
|
||||
# For Claude Desktop (standalone app)
|
||||
uv run fuzzforge mcp install claude-desktop
|
||||
uv run secpipe mcp install claude-desktop
|
||||
|
||||
# Verify installation
|
||||
uv run fuzzforge mcp status
|
||||
uv run secpipe mcp status
|
||||
```
|
||||
|
||||
**Restart your editor** and your AI agent will have access to FuzzForge tools!
|
||||
**Restart your editor** and your AI agent will have access to SecPipe tools!
|
||||
|
||||
---
|
||||
|
||||
## 📦 Modules
|
||||
## 🧑💻 Usage
|
||||
|
||||
FuzzForge modules are containerized security tools that AI agents can orchestrate. The module ecosystem is designed around a simple principle: **the OSS runtime orchestrates, enterprise modules execute**.
|
||||
Once installed, just talk to your AI agent:
|
||||
|
||||
### Module Ecosystem
|
||||
|
||||
| | FuzzForge AI | FuzzForge Enterprise Modules |
|
||||
|---|---|---|
|
||||
| **What** | Runtime & MCP server | Security research modules |
|
||||
| **License** | Apache 2.0 | BSL 1.1 (Business Source License) |
|
||||
| **Compatibility** | ✅ Runs any compatible module | ✅ Works with FuzzForge AI |
|
||||
|
||||
**Enterprise modules** are developed separately and provide production-ready security tooling:
|
||||
|
||||
| Category | Modules | Description |
|
||||
|----------|---------|-------------|
|
||||
| 🔍 **Static Analysis** | Rust Analyzer, Solidity Analyzer, Cairo Analyzer | Code analysis and fuzzable function detection |
|
||||
| 🎯 **Fuzzing** | Cargo Fuzzer, Honggfuzz, AFL++ | Coverage-guided fuzz testing |
|
||||
| 💥 **Crash Analysis** | Crash Triager, Root Cause Analyzer | Automated crash deduplication and analysis |
|
||||
| 🔐 **Vulnerability Detection** | Pattern Matcher, Taint Analyzer | Security vulnerability scanning |
|
||||
| 📝 **Reporting** | Report Generator, SARIF Exporter | Automated security report generation |
|
||||
|
||||
> 💡 **Build your own modules!** The FuzzForge SDK allows you to create custom modules that integrate seamlessly with FuzzForge AI. See [Creating Custom Modules](#-creating-custom-modules).
|
||||
|
||||
### Execution Modes
|
||||
|
||||
Modules run in two execution modes:
|
||||
|
||||
#### One-shot Execution
|
||||
|
||||
Run a module once and get results:
|
||||
|
||||
```python
|
||||
result = execute_module("my-analyzer", assets_path="/path/to/project")
|
||||
```
|
||||
"What security tools are available?"
|
||||
"Scan this firmware image for vulnerabilities"
|
||||
"Analyze this binary with radare2"
|
||||
"Run nuclei against https://example.com"
|
||||
```
|
||||
|
||||
#### Continuous Execution
|
||||
The agent will use SecPipe to discover the right hub tools, chain them into a pipeline, and return results — all without you touching a terminal.
|
||||
|
||||
For long-running tasks like fuzzing, with real-time metrics:
|
||||
|
||||
```python
|
||||
# Start continuous execution
|
||||
session = start_continuous_module("my-fuzzer",
|
||||
assets_path="/path/to/project",
|
||||
configuration={"target": "my_target"})
|
||||
|
||||
# Check status with live metrics
|
||||
status = get_continuous_status(session["session_id"])
|
||||
|
||||
# Stop and collect results
|
||||
stop_continuous_module(session["session_id"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Creating Custom Modules
|
||||
|
||||
Build your own security modules with the FuzzForge SDK:
|
||||
|
||||
```python
|
||||
from fuzzforge_modules_sdk import FuzzForgeModule, FuzzForgeModuleResults
|
||||
|
||||
class MySecurityModule(FuzzForgeModule):
|
||||
def _run(self, resources):
|
||||
self.emit_event("started", target=resources[0].path)
|
||||
|
||||
# Your analysis logic here
|
||||
results = self.analyze(resources)
|
||||
|
||||
self.emit_progress(100, status="completed",
|
||||
message=f"Analysis complete")
|
||||
return FuzzForgeModuleResults.SUCCESS
|
||||
```
|
||||
|
||||
📖 See the [Module SDK Guide](fuzzforge-modules/fuzzforge-modules-sdk/README.md) for details.
|
||||
See the [Usage Guide](USAGE.md) for detailed setup and advanced workflows.
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
fuzzforge_ai/
|
||||
├── fuzzforge-cli/ # Command-line interface
|
||||
├── fuzzforge-common/ # Shared abstractions (containers, storage)
|
||||
├── fuzzforge-mcp/ # MCP server for AI agents
|
||||
├── fuzzforge-modules/ # Security modules
|
||||
│ └── fuzzforge-modules-sdk/ # Module development SDK
|
||||
├── fuzzforge-runner/ # Local execution engine
|
||||
├── fuzzforge-types/ # Type definitions & schemas
|
||||
└── demo/ # Demo projects for testing
|
||||
secpipe_ai/
|
||||
├── secpipe-mcp/ # MCP server — the core of SecPipe
|
||||
├── secpipe-cli/ # Command-line interface & terminal UI
|
||||
├── secpipe-common/ # Shared abstractions (containers, storage)
|
||||
├── secpipe-runner/ # Container execution engine (Docker/Podman)
|
||||
├── secpipe-tests/ # Integration tests
|
||||
├── mcp-security-hub/ # Default hub: 36 offensive security MCP servers
|
||||
└── scripts/ # Hub image build scripts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ What's Next
|
||||
|
||||
**[MCP Security Hub](https://github.com/FuzzingLabs/mcp-security-hub) integration** — Bridge 175+ offensive security tools (Nmap, Nuclei, Ghidra, and more) into FuzzForge workflows, all orchestrated by AI agents.
|
||||
|
||||
See [ROADMAP.md](ROADMAP.md) for the full roadmap.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions from the community!
|
||||
@@ -274,7 +247,7 @@ We welcome contributions from the community!
|
||||
- 🐛 Report bugs via [GitHub Issues](../../issues)
|
||||
- 💡 Suggest features or improvements
|
||||
- 🔧 Submit pull requests
|
||||
- 📦 Share your custom modules
|
||||
- 🔌 Add new MCP servers to the [Security Hub](https://github.com/FuzzingLabs/mcp-security-hub)
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
@@ -289,4 +262,4 @@ BSL 1.1 - See [LICENSE](LICENSE) for details.
|
||||
<p align="center">
|
||||
<strong>Maintained by <a href="https://fuzzinglabs.com">FuzzingLabs</a></strong>
|
||||
<br>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
65
RELEASE_NOTES.md
Normal file
65
RELEASE_NOTES.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# v0.8.0 — MCP Hub Architecture
|
||||
|
||||
SecPipe AI v0.8.0 is a major architectural rewrite. The previous module system has been replaced by the **MCP Hub** architecture — SecPipe now acts as a meta-MCP server that connects AI agents to collections of containerized security tools, discovered and orchestrated at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Highlights
|
||||
|
||||
### MCP Hub System
|
||||
|
||||
SecPipe no longer ships its own security modules. Instead, it connects to **MCP tool hubs** — registries of Dockerized MCP servers that AI agents can discover, chain, and execute autonomously.
|
||||
|
||||
- **Runtime tool discovery** — agents call `list_hub_servers` and `discover_hub_tools` to find available tools
|
||||
- **Agent context convention** — hub tools provide built-in usage tips, workflow guidance, and domain knowledge so agents can use them without human intervention
|
||||
- **Category filtering** — servers are organized by category (`binary-analysis`, `web-security`, `reconnaissance`, etc.) for efficient discovery
|
||||
- **Persistent sessions** — stateful tools like Radare2 run in long-lived containers with `start_hub_server` / `stop_hub_server`
|
||||
- **Volume mounts** — project assets are automatically mounted into tool containers for seamless file access
|
||||
- **Continuous mode** — long-running tools (fuzzers) with real-time status via `start_continuous_hub_tool`
|
||||
|
||||
### MCP Security Hub Integration
|
||||
|
||||
Ships with built-in support for the [MCP Security Hub](https://github.com/FuzzingLabs/mcp-security-hub) — **36 production-ready MCP servers** covering:
|
||||
|
||||
| Category | Servers | Examples |
|
||||
|----------|---------|----------|
|
||||
| Reconnaissance | 8 | Nmap, Masscan, Shodan, WhatWeb |
|
||||
| Web Security | 6 | Nuclei, SQLMap, ffuf, Nikto |
|
||||
| Binary Analysis | 6 | Radare2, Binwalk, YARA, Capa, Ghidra |
|
||||
| Blockchain | 3 | Medusa, Solazy, DAML Viewer |
|
||||
| Cloud Security | 3 | Trivy, Prowler, RoadRecon |
|
||||
| Code Security | 1 | Semgrep |
|
||||
| Secrets Detection | 1 | Gitleaks |
|
||||
| Exploitation | 1 | SearchSploit |
|
||||
| Fuzzing | 2 | Boofuzz, Dharma |
|
||||
| OSINT | 2 | Maigret, DNSTwist |
|
||||
| Threat Intel | 2 | VirusTotal, AlienVault OTX |
|
||||
| Active Directory | 1 | BloodHound |
|
||||
|
||||
> **185+ individual security tools** accessible through a single MCP connection.
|
||||
|
||||
### Terminal UI
|
||||
|
||||
A new interactive terminal interface (`uv run secpipe ui`) for managing hubs and agents:
|
||||
|
||||
- Dashboard with hub status overview
|
||||
- One-click MCP server installation for GitHub Copilot, Claude Code, and Claude Desktop
|
||||
- In-UI Docker image building with live log viewer
|
||||
- Hub linking and registry management
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- The module system has been removed (`list_modules`, `execute_module`, `start_continuous_module`)
|
||||
- Replaced by hub tools: `list_hub_servers`, `discover_hub_tools`, `execute_hub_tool`, `start_hub_server`, `stop_hub_server`, etc.
|
||||
- `make build-modules` replaced by `./scripts/build-hub-images.sh`
|
||||
|
||||
---
|
||||
|
||||
## Other Changes
|
||||
|
||||
- **CI**: GitHub Actions workflows with ruff lint, mypy typecheck, and tests
|
||||
- **Config**: `SECPIPE_USER_DIR` environment variable to override user-global data directory
|
||||
- **Storage**: `~/.secpipe` for user-global data, `.secpipe/` in workspace for project storage
|
||||
- **Docs**: README rewritten for hub-centric architecture
|
||||
20
ROADMAP.md
20
ROADMAP.md
@@ -1,6 +1,6 @@
|
||||
# FuzzForge AI Roadmap
|
||||
# SecPipe AI Roadmap
|
||||
|
||||
This document outlines the planned features and development direction for FuzzForge AI.
|
||||
This document outlines the planned features and development direction for SecPipe AI.
|
||||
|
||||
---
|
||||
|
||||
@@ -10,27 +10,27 @@ This document outlines the planned features and development direction for FuzzFo
|
||||
|
||||
**Status:** 🔄 Planned
|
||||
|
||||
Integrate [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub) tools into FuzzForge, giving AI agents access to 28 MCP servers and 163+ security tools through a unified interface.
|
||||
Integrate [mcp-security-hub](https://github.com/FuzzingLabs/mcp-security-hub) tools into SecPipe, giving AI agents access to 28 MCP servers and 163+ security tools through a unified interface.
|
||||
|
||||
#### How It Works
|
||||
|
||||
Unlike native FuzzForge modules (built with the SDK), mcp-security-hub tools are **standalone MCP servers**. The integration will bridge these tools so they can be:
|
||||
Unlike native SecPipe modules (built with the SDK), mcp-security-hub tools are **standalone MCP servers**. The integration will bridge these tools so they can be:
|
||||
|
||||
- Discovered via `list_modules` alongside native modules
|
||||
- Executed through FuzzForge's orchestration layer
|
||||
- Executed through SecPipe's orchestration layer
|
||||
- Chained with native modules in workflows
|
||||
|
||||
| Aspect | Native Modules | MCP Hub Tools |
|
||||
|--------|----------------|---------------|
|
||||
| **Runtime** | FuzzForge SDK container | Standalone MCP server container |
|
||||
| **Runtime** | SecPipe SDK container | Standalone MCP server container |
|
||||
| **Protocol** | Direct execution | MCP-to-MCP bridge |
|
||||
| **Configuration** | Module config | Tool-specific args |
|
||||
| **Output** | FuzzForge results format | Tool-native format (normalized) |
|
||||
| **Output** | SecPipe results format | Tool-native format (normalized) |
|
||||
|
||||
#### Goals
|
||||
|
||||
- Unified discovery of all available tools (native + hub)
|
||||
- Orchestrate hub tools through FuzzForge's workflow engine
|
||||
- Orchestrate hub tools through SecPipe's workflow engine
|
||||
- Normalize outputs for consistent result handling
|
||||
- No modification required to mcp-security-hub tools
|
||||
|
||||
@@ -65,7 +65,7 @@ AI Agent:
|
||||
|
||||
**Status:** 🔄 Planned
|
||||
|
||||
A graphical interface to manage FuzzForge without the command line.
|
||||
A graphical interface to manage SecPipe without the command line.
|
||||
|
||||
#### Goals
|
||||
|
||||
@@ -115,7 +115,7 @@ Features under consideration for future releases:
|
||||
|
||||
Have suggestions for the roadmap?
|
||||
|
||||
- Open an issue on [GitHub](https://github.com/FuzzingLabs/fuzzforge_ai/issues)
|
||||
- Open an issue on [GitHub](https://github.com/FuzzingLabs/secpipe_ai/issues)
|
||||
- Join our [Discord](https://discord.gg/8XEX33UUwZ)
|
||||
|
||||
---
|
||||
|
||||
490
USAGE.md
490
USAGE.md
@@ -1,8 +1,9 @@
|
||||
# FuzzForge AI Usage Guide
|
||||
# SecPipe 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 SecPipe 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.
|
||||
> **SecPipe is designed to be used with AI agents** (GitHub Copilot, Claude, etc.) via MCP.
|
||||
> A terminal UI (`secpipe 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.
|
||||
|
||||
---
|
||||
@@ -12,12 +13,21 @@ This guide covers everything you need to know to get started with FuzzForge AI -
|
||||
- [Quick Start](#quick-start)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Building Modules](#building-modules)
|
||||
- [MCP Server Configuration](#mcp-server-configuration)
|
||||
- [Terminal UI](#terminal-ui)
|
||||
- [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)
|
||||
- [Claude Code (CLI)](#claude-code-cli)
|
||||
- [Claude Desktop](#claude-desktop)
|
||||
- [Using FuzzForge with AI](#using-fuzzforge-with-ai)
|
||||
- [Using SecPipe with AI](#using-secpipe-with-ai)
|
||||
- [CLI Reference](#cli-reference)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
@@ -27,41 +37,57 @@ This guide covers everything you need to know to get started with FuzzForge AI -
|
||||
## Quick Start
|
||||
|
||||
> **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
|
||||
# 1. Clone and install
|
||||
git clone https://github.com/FuzzingLabs/fuzzforge_ai.git
|
||||
cd fuzzforge_ai
|
||||
git clone https://github.com/FuzzingLabs/secpipe_ai.git
|
||||
cd secpipe_ai
|
||||
uv sync
|
||||
|
||||
# 2. Build the module images (one-time setup)
|
||||
make build-modules
|
||||
# 2. Launch the terminal UI
|
||||
uv run secpipe ui
|
||||
|
||||
# 3. 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
|
||||
# 3. Press 'h' → "FuzzingLabs Hub" to clone & link the default security hub
|
||||
# 4. Select an agent row and press Enter to install the MCP server for your agent
|
||||
# 5. Build the Docker images for the hub tools (required before tools can run)
|
||||
./scripts/build-hub-images.sh
|
||||
|
||||
# 4. Restart your AI agent (VS Code, Claude, etc.)
|
||||
|
||||
# 5. Start talking to your AI:
|
||||
# "List available FuzzForge modules"
|
||||
# 6. Restart your AI agent and start talking:
|
||||
# "What security tools are available?"
|
||||
# "Scan this binary with binwalk and yara"
|
||||
# "Analyze this Rust crate for fuzzable functions"
|
||||
# "Start fuzzing the parse_input function"
|
||||
```
|
||||
|
||||
> **Note:** FuzzForge uses Docker by default. Podman is also supported via `--engine podman`.
|
||||
Or do it entirely from the command line:
|
||||
|
||||
```bash
|
||||
# Install MCP for your AI agent
|
||||
uv run secpipe mcp install copilot # For VS Code + GitHub Copilot
|
||||
# OR
|
||||
uv run secpipe mcp install claude-code # For Claude Code CLI
|
||||
|
||||
# Clone and link the default security hub
|
||||
git clone git@github.com:FuzzingLabs/mcp-security-hub.git ~/.secpipe/hubs/mcp-security-hub
|
||||
|
||||
# Build hub tool images (required — tools only run once their image is built)
|
||||
./scripts/build-hub-images.sh
|
||||
|
||||
# Restart your AI agent — done!
|
||||
```
|
||||
|
||||
> **Note:** SecPipe uses Docker by default. Podman is also supported via `--engine podman`.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing FuzzForge AI, ensure you have:
|
||||
Before installing SecPipe AI, ensure you have:
|
||||
|
||||
- **Python 3.12+** - [Download Python](https://www.python.org/downloads/)
|
||||
- **uv** package manager - [Install uv](https://docs.astral.sh/uv/)
|
||||
- **Docker** - Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
||||
- **Python 3.12+** — [Download Python](https://www.python.org/downloads/)
|
||||
- **uv** package manager — [Install uv](https://docs.astral.sh/uv/)
|
||||
- **Docker** — Container runtime ([Install Docker](https://docs.docker.com/get-docker/))
|
||||
- **Git** — For cloning hub repositories
|
||||
|
||||
### Installing uv
|
||||
|
||||
@@ -86,7 +112,7 @@ sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
> **Note:** Podman is also supported. Use `--engine podman` with CLI commands
|
||||
> or set `FUZZFORGE_ENGINE=podman` environment variable.
|
||||
> or set `SECPIPE_ENGINE=podman` environment variable.
|
||||
|
||||
---
|
||||
|
||||
@@ -95,8 +121,8 @@ sudo usermod -aG docker $USER
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/FuzzingLabs/fuzzforge_ai.git
|
||||
cd fuzzforge_ai
|
||||
git clone https://github.com/FuzzingLabs/secpipe_ai.git
|
||||
cd secpipe_ai
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
@@ -105,254 +131,296 @@ cd fuzzforge_ai
|
||||
uv sync
|
||||
```
|
||||
|
||||
This installs all FuzzForge components in a virtual environment.
|
||||
This installs all SecPipe components in a virtual environment.
|
||||
|
||||
### 3. Verify Installation
|
||||
|
||||
```bash
|
||||
uv run fuzzforge --help
|
||||
uv run secpipe --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building Modules
|
||||
## Terminal UI
|
||||
|
||||
FuzzForge modules are containerized security tools. After cloning, you need to build them once:
|
||||
SecPipe 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
|
||||
# From the fuzzforge_ai directory
|
||||
make build-modules
|
||||
uv run secpipe ui
|
||||
```
|
||||
|
||||
This builds all available modules:
|
||||
- `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
|
||||
### Dashboard
|
||||
|
||||
### Build a Single Module
|
||||
The main screen is split into two panels:
|
||||
|
||||
```bash
|
||||
# Build a specific module
|
||||
cd fuzzforge-modules/rust-analyzer
|
||||
make build
|
||||
```
|
||||
| Panel | Content |
|
||||
|-------|---------|
|
||||
| **AI Agents** (left) | Shows GitHub Copilot, Claude Desktop, and Claude Code with live link status and config file path |
|
||||
| **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
|
||||
# List built module images
|
||||
docker images | grep fuzzforge
|
||||
```
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `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:
|
||||
```
|
||||
fuzzforge-rust-analyzer 0.1.0 abc123def456 2 minutes ago 850 MB
|
||||
fuzzforge-cargo-fuzzer 0.1.0 789ghi012jkl 2 minutes ago 1.2 GB
|
||||
...
|
||||
```
|
||||
### Agent Setup
|
||||
|
||||
Select an agent row in the AI Agents table and press `Enter`:
|
||||
|
||||
- **If the agent is not linked** → a setup dialog opens asking for your container engine (Docker or Podman), then installs the SecPipe MCP configuration
|
||||
- **If the agent is already linked** → a confirmation dialog offers to unlink it (removes the `secpipe` entry without touching other MCP servers)
|
||||
|
||||
The setup auto-detects:
|
||||
- SecPipe 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 `~/.secpipe/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, SecPipe 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
|
||||
└── ...
|
||||
```
|
||||
|
||||
SecPipe 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 secpipe 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 ~/.secpipe/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
|
||||
|
||||
```bash
|
||||
# That's it! Just run this command:
|
||||
uv run fuzzforge mcp install copilot
|
||||
uv run secpipe mcp install copilot
|
||||
```
|
||||
|
||||
The command auto-detects everything:
|
||||
- **FuzzForge root** - Where FuzzForge is installed
|
||||
- **Modules path** - Defaults to `fuzzforge_ai/fuzzforge-modules`
|
||||
- **Docker socket** - Auto-detects `/var/run/docker.sock`
|
||||
The command auto-detects:
|
||||
- **SecPipe root** — Where SecPipe is installed
|
||||
- **Docker socket** — Auto-detects `/var/run/docker.sock`
|
||||
|
||||
**Optional overrides** (usually not needed):
|
||||
**Optional overrides:**
|
||||
```bash
|
||||
uv run fuzzforge mcp install copilot \
|
||||
--modules /path/to/modules \
|
||||
--engine podman # if using Podman instead of Docker
|
||||
uv run secpipe mcp install copilot --engine podman
|
||||
```
|
||||
|
||||
**After installation:**
|
||||
1. Restart VS Code
|
||||
2. Open GitHub Copilot Chat
|
||||
3. FuzzForge tools are now available!
|
||||
**After installation:** Restart VS Code. SecPipe tools appear in GitHub Copilot Chat.
|
||||
|
||||
### Claude Code (CLI)
|
||||
|
||||
```bash
|
||||
uv run fuzzforge mcp install claude-code
|
||||
uv run secpipe mcp install claude-code
|
||||
```
|
||||
|
||||
Installs to `~/.claude.json` so FuzzForge tools are available from any directory.
|
||||
|
||||
**After installation:**
|
||||
1. Run `claude` from any directory
|
||||
2. FuzzForge tools are now available!
|
||||
Installs to `~/.claude.json`. SecPipe tools are available from any directory after restarting Claude.
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
```bash
|
||||
# Automatic installation
|
||||
uv run fuzzforge mcp install claude-desktop
|
||||
|
||||
# Verify
|
||||
uv run fuzzforge mcp status
|
||||
uv run secpipe mcp install claude-desktop
|
||||
```
|
||||
|
||||
**After installation:**
|
||||
1. Restart Claude Desktop
|
||||
2. FuzzForge tools are now available!
|
||||
**After installation:** Restart Claude Desktop.
|
||||
|
||||
### Check MCP Status
|
||||
### Check Status
|
||||
|
||||
```bash
|
||||
uv run fuzzforge mcp status
|
||||
uv run secpipe mcp status
|
||||
```
|
||||
|
||||
Shows configuration status for all supported AI agents:
|
||||
|
||||
```
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ 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
|
||||
### Remove Configuration
|
||||
|
||||
```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
|
||||
uv run fuzzforge mcp uninstall copilot
|
||||
uv run fuzzforge mcp uninstall claude-desktop
|
||||
uv run fuzzforge mcp uninstall claude-code
|
||||
uv run secpipe mcp uninstall copilot
|
||||
uv run secpipe mcp uninstall claude-code
|
||||
uv run secpipe mcp uninstall claude-desktop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using FuzzForge with AI
|
||||
## Using SecPipe 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 SecPipe through natural language with your AI assistant.
|
||||
|
||||
### Example Conversations
|
||||
|
||||
**Discover available tools:**
|
||||
```
|
||||
You: "What FuzzForge modules are available?"
|
||||
AI: Uses list_modules → "I found 4 modules: rust-analyzer, cargo-fuzzer,
|
||||
harness-validator, and crash-analyzer..."
|
||||
You: "What security tools are available in SecPipe?"
|
||||
AI: Queries hub tools → "I found 15 tools across categories: nmap for
|
||||
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"
|
||||
AI: Uses execute_module("rust-analyzer") → "I found 3 good fuzzing candidates:
|
||||
- parse_input() in src/parser.rs - handles untrusted input
|
||||
- decode_message() in src/codec.rs - complex parsing logic
|
||||
..."
|
||||
```
|
||||
AI: Uses rust-analyzer → "Found 3 fuzzable entry points..."
|
||||
|
||||
**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"
|
||||
AI: Uses start_continuous_module("cargo-fuzzer") → "Started fuzzing session abc123"
|
||||
|
||||
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..."
|
||||
AI: Uses cargo-fuzzer → "Fuzzing session started. 2 crashes found..."
|
||||
```
|
||||
|
||||
### Available MCP Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_modules` | List all available security modules |
|
||||
| `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 |
|
||||
**Scan for vulnerabilities:**
|
||||
```
|
||||
You: "Scan this codebase with semgrep for security issues"
|
||||
AI: Uses semgrep-mcp → "Found 5 findings: 2 high severity SQL injection
|
||||
patterns, 3 medium severity hardcoded secrets..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Reference
|
||||
|
||||
> **Note:** The CLI is for advanced users. Most users should interact with FuzzForge through their AI assistant.
|
||||
### UI Command
|
||||
|
||||
```bash
|
||||
uv run secpipe ui # Launch the terminal dashboard
|
||||
```
|
||||
|
||||
### MCP Commands
|
||||
|
||||
```bash
|
||||
uv run fuzzforge mcp status # Check configuration status
|
||||
uv run fuzzforge mcp install <agent> # Install MCP config
|
||||
uv run fuzzforge mcp uninstall <agent> # Remove MCP config
|
||||
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
|
||||
uv run secpipe mcp status # Check agent configuration status
|
||||
uv run secpipe mcp install <agent> # Install MCP config (copilot|claude-code|claude-desktop)
|
||||
uv run secpipe mcp uninstall <agent> # Remove MCP config
|
||||
uv run secpipe mcp generate <agent> # Preview config without installing
|
||||
```
|
||||
|
||||
### Project Commands
|
||||
|
||||
```bash
|
||||
uv run fuzzforge project init # Initialize a project
|
||||
uv run fuzzforge project info # Show project info
|
||||
uv run fuzzforge project executions # List executions
|
||||
uv run fuzzforge project results <id> # Get execution results
|
||||
uv run secpipe project init # Initialize a project
|
||||
uv run secpipe project info # Show project info
|
||||
uv run secpipe project executions # List executions
|
||||
uv run secpipe project results <id> # Get execution results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Configure FuzzForge using environment variables:
|
||||
Configure SecPipe using environment variables:
|
||||
|
||||
```bash
|
||||
# Project paths
|
||||
export FUZZFORGE_MODULES_PATH=/path/to/modules
|
||||
export FUZZFORGE_STORAGE_PATH=/path/to/storage
|
||||
# Override the SecPipe installation root (auto-detected from cwd by default)
|
||||
export SECPIPE_ROOT=/path/to/secpipe_ai
|
||||
|
||||
# Override the user-global data directory (default: ~/.secpipe)
|
||||
# Useful for isolated testing without touching your real installation
|
||||
export SECPIPE_USER_DIR=/tmp/my-secpipe-test
|
||||
|
||||
# Storage path for projects and execution results (default: <workspace>/.secpipe/storage)
|
||||
export SECPIPE_STORAGE__PATH=/path/to/storage
|
||||
|
||||
# Container engine (Docker is default)
|
||||
export FUZZFORGE_ENGINE__TYPE=docker # or podman
|
||||
export SECPIPE_ENGINE__TYPE=docker # or podman
|
||||
|
||||
# Podman-specific settings (only needed if using Podman under Snap)
|
||||
export FUZZFORGE_ENGINE__GRAPHROOT=~/.fuzzforge/containers/storage
|
||||
export FUZZFORGE_ENGINE__RUNROOT=~/.fuzzforge/containers/run
|
||||
# Podman-specific container storage paths
|
||||
export SECPIPE_ENGINE__GRAPHROOT=~/.secpipe/containers/storage
|
||||
export SECPIPE_ENGINE__RUNROOT=~/.secpipe/containers/run
|
||||
```
|
||||
|
||||
---
|
||||
@@ -384,66 +452,62 @@ Error: Permission denied connecting to Docker socket
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Add your user to the docker group
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Log out and back in for changes to take effect
|
||||
# Then verify:
|
||||
# Log out and back in, then verify:
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
### No Modules Found
|
||||
### Hub Images Not Built
|
||||
|
||||
```
|
||||
No modules found.
|
||||
```
|
||||
The dashboard shows ✗ Not built for tools:
|
||||
|
||||
**Solution:**
|
||||
1. Build the modules first: `make build-modules`
|
||||
2. Check the modules path: `uv run fuzzforge modules list`
|
||||
3. Verify images exist: `docker images | grep fuzzforge`
|
||||
```bash
|
||||
# Build all hub images
|
||||
./scripts/build-hub-images.sh
|
||||
|
||||
# Or build a single tool
|
||||
docker build -t <tool-name>:latest mcp-security-hub/<category>/<tool-name>/
|
||||
```
|
||||
|
||||
### MCP Server Not Starting
|
||||
|
||||
Check the MCP configuration:
|
||||
```bash
|
||||
uv run fuzzforge mcp status
|
||||
```
|
||||
# Check agent configuration
|
||||
uv run secpipe mcp status
|
||||
|
||||
Verify the configuration file path exists and contains valid JSON.
|
||||
|
||||
### Module Container Fails to Build
|
||||
|
||||
```bash
|
||||
# Build module container manually to see errors
|
||||
cd fuzzforge-modules/<module-name>
|
||||
docker build -t <module-name> .
|
||||
# Verify the config file path exists and contains valid JSON
|
||||
cat ~/.config/Code/User/mcp.json # Copilot
|
||||
cat ~/.claude.json # Claude Code
|
||||
```
|
||||
|
||||
### Using Podman Instead of Docker
|
||||
|
||||
If you prefer Podman:
|
||||
```bash
|
||||
# Use --engine podman with CLI
|
||||
uv run fuzzforge mcp install copilot --engine podman
|
||||
# Install with Podman engine
|
||||
uv run secpipe mcp install copilot --engine podman
|
||||
|
||||
# Or set environment variable
|
||||
export FUZZFORGE_ENGINE=podman
|
||||
export SECPIPE_ENGINE=podman
|
||||
```
|
||||
|
||||
### Check Logs
|
||||
### Hub Registry
|
||||
|
||||
SecPipe stores linked hub information in `~/.secpipe/hubs.json`. If something goes wrong:
|
||||
|
||||
FuzzForge stores execution logs in the storage directory:
|
||||
```bash
|
||||
ls -la ~/.fuzzforge/storage/<project-id>/<execution-id>/
|
||||
# View registry
|
||||
cat ~/.secpipe/hubs.json
|
||||
|
||||
# Reset registry
|
||||
rm ~/.secpipe/hubs.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- 📖 Read the [Module SDK Guide](fuzzforge-modules/fuzzforge-modules-sdk/README.md) to create custom modules
|
||||
- 🎬 Check the demos in the [README](README.md)
|
||||
- 🖥️ Launch `uv run secpipe ui` and explore the dashboard
|
||||
- 🔒 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
|
||||
|
||||
---
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 360 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
@@ -1,3 +0,0 @@
|
||||
# FuzzForge CLI
|
||||
|
||||
...
|
||||
@@ -1,15 +0,0 @@
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [ "ALL" ]
|
||||
ignore = [
|
||||
"COM812", # conflicts with the formatter
|
||||
"D203", # conflicts with 'D211'
|
||||
"D213", # conflicts with 'D212'
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"PLR2004", # allowing comparisons using unamed numerical constants in tests
|
||||
"S101", # allowing 'assert' statements in tests
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
"""FuzzForge CLI application."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from typer import Context as TyperContext
|
||||
from typer import Option, Typer
|
||||
|
||||
from fuzzforge_cli.commands import mcp, projects
|
||||
from fuzzforge_cli.context import Context
|
||||
from fuzzforge_mcp.storage import LocalStorage
|
||||
|
||||
application: Typer = Typer(
|
||||
name="fuzzforge",
|
||||
help="FuzzForge AI - Security research orchestration platform.",
|
||||
)
|
||||
|
||||
|
||||
@application.callback()
|
||||
def main(
|
||||
project_path: Annotated[
|
||||
Path,
|
||||
Option(
|
||||
"--project",
|
||||
"-p",
|
||||
envvar="FUZZFORGE_PROJECT__DEFAULT_PATH",
|
||||
help="Path to the FuzzForge project directory.",
|
||||
),
|
||||
] = Path.cwd(),
|
||||
storage_path: Annotated[
|
||||
Path,
|
||||
Option(
|
||||
"--storage",
|
||||
envvar="FUZZFORGE_STORAGE__PATH",
|
||||
help="Path to the storage directory.",
|
||||
),
|
||||
] = Path.home() / ".fuzzforge" / "storage",
|
||||
context: TyperContext = None, # type: ignore[assignment]
|
||||
) -> None:
|
||||
"""FuzzForge AI - Security research orchestration platform.
|
||||
|
||||
Discover and execute MCP hub tools for security research.
|
||||
|
||||
"""
|
||||
storage = LocalStorage(storage_path=storage_path)
|
||||
|
||||
context.obj = Context(
|
||||
storage=storage,
|
||||
project_path=project_path,
|
||||
)
|
||||
|
||||
|
||||
application.add_typer(mcp.application)
|
||||
application.add_typer(projects.application)
|
||||
@@ -1,3 +0,0 @@
|
||||
# FuzzForge Common
|
||||
|
||||
...
|
||||
@@ -1,20 +0,0 @@
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [ "ALL" ]
|
||||
ignore = [
|
||||
"COM812", # conflicts with the formatter
|
||||
"D100", # ignoring missing docstrings in public modules
|
||||
"D104", # ignoring missing docstrings in public packages
|
||||
"D203", # conflicts with 'D211'
|
||||
"D213", # conflicts with 'D212'
|
||||
"TD002", # ignoring missing author in 'TODO' statements
|
||||
"TD003", # ignoring missing issue link in 'TODO' statements
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"ANN401", # allowing 'typing.Any' to be used to type function parameters in tests
|
||||
"PLR2004", # allowing comparisons using unamed numerical constants in tests
|
||||
"S101", # allowing 'assert' statements in tests
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
"""FuzzForge Common - Shared abstractions and implementations for FuzzForge.
|
||||
|
||||
This package provides:
|
||||
- Sandbox engine abstractions (Podman, Docker)
|
||||
- Common exceptions
|
||||
|
||||
Example usage:
|
||||
from fuzzforge_common import (
|
||||
AbstractFuzzForgeSandboxEngine,
|
||||
ImageInfo,
|
||||
Podman,
|
||||
PodmanConfiguration,
|
||||
)
|
||||
"""
|
||||
|
||||
from fuzzforge_common.exceptions import FuzzForgeError
|
||||
from fuzzforge_common.sandboxes import (
|
||||
AbstractFuzzForgeEngineConfiguration,
|
||||
AbstractFuzzForgeSandboxEngine,
|
||||
Docker,
|
||||
DockerConfiguration,
|
||||
FuzzForgeSandboxEngines,
|
||||
ImageInfo,
|
||||
Podman,
|
||||
PodmanConfiguration,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbstractFuzzForgeEngineConfiguration",
|
||||
"AbstractFuzzForgeSandboxEngine",
|
||||
"Docker",
|
||||
"DockerConfiguration",
|
||||
"FuzzForgeError",
|
||||
"FuzzForgeSandboxEngines",
|
||||
"ImageInfo",
|
||||
"Podman",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
"""FuzzForge sandbox abstractions and implementations."""
|
||||
|
||||
from fuzzforge_common.sandboxes.engines import (
|
||||
AbstractFuzzForgeEngineConfiguration,
|
||||
AbstractFuzzForgeSandboxEngine,
|
||||
Docker,
|
||||
DockerConfiguration,
|
||||
FuzzForgeSandboxEngines,
|
||||
ImageInfo,
|
||||
Podman,
|
||||
PodmanConfiguration,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbstractFuzzForgeEngineConfiguration",
|
||||
"AbstractFuzzForgeSandboxEngine",
|
||||
"Docker",
|
||||
"DockerConfiguration",
|
||||
"FuzzForgeSandboxEngines",
|
||||
"ImageInfo",
|
||||
"Podman",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Container engine implementations for FuzzForge sandboxes."""
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.base import (
|
||||
AbstractFuzzForgeEngineConfiguration,
|
||||
AbstractFuzzForgeSandboxEngine,
|
||||
ImageInfo,
|
||||
)
|
||||
from fuzzforge_common.sandboxes.engines.docker import Docker, DockerConfiguration
|
||||
from fuzzforge_common.sandboxes.engines.enumeration import FuzzForgeSandboxEngines
|
||||
from fuzzforge_common.sandboxes.engines.podman import Podman, PodmanConfiguration
|
||||
|
||||
__all__ = [
|
||||
"AbstractFuzzForgeEngineConfiguration",
|
||||
"AbstractFuzzForgeSandboxEngine",
|
||||
"Docker",
|
||||
"DockerConfiguration",
|
||||
"FuzzForgeSandboxEngines",
|
||||
"ImageInfo",
|
||||
"Podman",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -1,15 +0,0 @@
|
||||
"""Base engine abstractions."""
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.base.configuration import (
|
||||
AbstractFuzzForgeEngineConfiguration,
|
||||
)
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import (
|
||||
AbstractFuzzForgeSandboxEngine,
|
||||
ImageInfo,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbstractFuzzForgeEngineConfiguration",
|
||||
"AbstractFuzzForgeSandboxEngine",
|
||||
"ImageInfo",
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.enumeration import (
|
||||
FuzzForgeSandboxEngines, # noqa: TC001 (required by 'pydantic' at runtime)
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine
|
||||
|
||||
|
||||
class AbstractFuzzForgeEngineConfiguration(ABC, BaseModel):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
kind: FuzzForgeSandboxEngines
|
||||
|
||||
@abstractmethod
|
||||
def into_engine(self) -> AbstractFuzzForgeSandboxEngine:
|
||||
"""TODO."""
|
||||
message: str = f"method 'into_engine' is not implemented for class '{self.__class__.__name__}'"
|
||||
raise NotImplementedError(message)
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Docker container engine implementation."""
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.docker.cli import DockerCLI
|
||||
from fuzzforge_common.sandboxes.engines.docker.configuration import (
|
||||
DockerConfiguration,
|
||||
)
|
||||
from fuzzforge_common.sandboxes.engines.docker.engine import Docker
|
||||
|
||||
__all__ = [
|
||||
"Docker",
|
||||
"DockerCLI",
|
||||
"DockerConfiguration",
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.base.configuration import AbstractFuzzForgeEngineConfiguration
|
||||
from fuzzforge_common.sandboxes.engines.docker.engine import Docker
|
||||
from fuzzforge_common.sandboxes.engines.enumeration import FuzzForgeSandboxEngines
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine
|
||||
|
||||
|
||||
class DockerConfiguration(AbstractFuzzForgeEngineConfiguration):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
kind: Literal[FuzzForgeSandboxEngines.DOCKER] = FuzzForgeSandboxEngines.DOCKER
|
||||
|
||||
#: TODO.
|
||||
socket: str
|
||||
|
||||
def into_engine(self) -> AbstractFuzzForgeSandboxEngine:
|
||||
"""TODO."""
|
||||
return Docker(socket=self.socket)
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Podman container engine implementation."""
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.podman.cli import PodmanCLI
|
||||
from fuzzforge_common.sandboxes.engines.podman.configuration import (
|
||||
PodmanConfiguration,
|
||||
)
|
||||
from fuzzforge_common.sandboxes.engines.podman.engine import Podman
|
||||
|
||||
__all__ = [
|
||||
"Podman",
|
||||
"PodmanCLI",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.base.configuration import AbstractFuzzForgeEngineConfiguration
|
||||
from fuzzforge_common.sandboxes.engines.enumeration import FuzzForgeSandboxEngines
|
||||
from fuzzforge_common.sandboxes.engines.podman.engine import Podman
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine
|
||||
|
||||
|
||||
class PodmanConfiguration(AbstractFuzzForgeEngineConfiguration):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
kind: Literal[FuzzForgeSandboxEngines.PODMAN] = FuzzForgeSandboxEngines.PODMAN
|
||||
|
||||
#: TODO.
|
||||
socket: str
|
||||
|
||||
def into_engine(self) -> AbstractFuzzForgeSandboxEngine:
|
||||
"""TODO."""
|
||||
return Podman(socket=self.socket)
|
||||
@@ -1 +0,0 @@
|
||||
pytest_plugins = ["fuzzforge_tests.fixtures"]
|
||||
@@ -1,16 +0,0 @@
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [ "ALL" ]
|
||||
ignore = [
|
||||
"COM812", # conflicts with the formatter
|
||||
"D203", # conflicts with 'D211'
|
||||
"D213", # conflicts with 'D212'
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"PLR0913", # allowing functions with many arguments in tests (required for fixtures)
|
||||
"PLR2004", # allowing comparisons using unamed numerical constants in tests
|
||||
"S101", # allowing 'assert' statements in tests
|
||||
]
|
||||
@@ -1,70 +0,0 @@
|
||||
"""FuzzForge MCP Server Application.
|
||||
|
||||
This is the main entry point for the FuzzForge MCP server, providing
|
||||
AI agents with tools to discover and execute MCP hub tools for
|
||||
security research.
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
|
||||
|
||||
from fuzzforge_mcp import resources, tools
|
||||
from fuzzforge_mcp.settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncGenerator
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastMCP) -> AsyncGenerator[Settings]:
|
||||
"""Initialize MCP server lifespan context.
|
||||
|
||||
Loads settings from environment variables and makes them
|
||||
available to all tools and resources.
|
||||
|
||||
:param mcp: FastMCP server instance (unused).
|
||||
:return: Settings instance for dependency injection.
|
||||
|
||||
"""
|
||||
settings: Settings = Settings()
|
||||
yield settings
|
||||
|
||||
|
||||
mcp: FastMCP = FastMCP(
|
||||
name="FuzzForge MCP Server",
|
||||
instructions="""
|
||||
FuzzForge is a security research orchestration platform. Use these tools to:
|
||||
|
||||
1. **List hub servers**: Discover registered MCP tool servers
|
||||
2. **Discover tools**: Find available tools from hub servers
|
||||
3. **Execute hub tools**: Run security tools in isolated containers
|
||||
4. **Manage projects**: Initialize and configure projects
|
||||
5. **Get results**: Retrieve execution results
|
||||
|
||||
Typical workflow:
|
||||
1. Initialize a project with `init_project`
|
||||
2. Set project assets with `set_project_assets` (optional, only needed once for the source directory)
|
||||
3. List available hub servers with `list_hub_servers`
|
||||
4. Discover tools from servers with `discover_hub_tools`
|
||||
5. Execute hub tools with `execute_hub_tool`
|
||||
|
||||
Hub workflow:
|
||||
1. List available hub servers with `list_hub_servers`
|
||||
2. Discover tools from servers with `discover_hub_tools`
|
||||
3. Execute hub tools with `execute_hub_tool`
|
||||
""",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
mcp.add_middleware(middleware=ErrorHandlingMiddleware())
|
||||
|
||||
mcp.mount(resources.mcp)
|
||||
mcp.mount(tools.mcp)
|
||||
|
||||
# HTTP app for testing (primary mode is stdio)
|
||||
app = mcp.http_app()
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"""TODO."""
|
||||
|
||||
|
||||
class FuzzForgeMCPError(Exception):
|
||||
"""TODO."""
|
||||
@@ -1,203 +0,0 @@
|
||||
"""FuzzForge MCP Server - Local project storage.
|
||||
|
||||
Lightweight project storage for managing `.fuzzforge/` directories,
|
||||
execution results, and project configuration. Extracted from the
|
||||
former fuzzforge-runner storage module.
|
||||
|
||||
Storage is placed directly in the project directory as `.fuzzforge/`
|
||||
for maximum visibility and ease of debugging.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tarfile import open as Archive # noqa: N812
|
||||
|
||||
logger = logging.getLogger("fuzzforge-mcp")
|
||||
|
||||
#: Name of the FuzzForge storage directory within projects.
|
||||
FUZZFORGE_DIR_NAME: str = ".fuzzforge"
|
||||
|
||||
#: Standard results archive filename.
|
||||
RESULTS_ARCHIVE_FILENAME: str = "results.tar.gz"
|
||||
|
||||
|
||||
class StorageError(Exception):
|
||||
"""Raised when a storage operation fails."""
|
||||
|
||||
|
||||
class LocalStorage:
|
||||
"""Local filesystem storage backend for FuzzForge.
|
||||
|
||||
Provides lightweight storage for project configuration and
|
||||
execution results tracking.
|
||||
|
||||
Directory structure (inside project directory)::
|
||||
|
||||
{project_path}/.fuzzforge/
|
||||
config.json # Project config (source path reference)
|
||||
runs/ # Execution results
|
||||
{execution_id}/
|
||||
results.tar.gz
|
||||
|
||||
"""
|
||||
|
||||
_base_path: Path
|
||||
|
||||
def __init__(self, base_path: Path) -> None:
|
||||
"""Initialize storage backend.
|
||||
|
||||
:param base_path: Root directory for global storage (fallback).
|
||||
|
||||
"""
|
||||
self._base_path = base_path
|
||||
self._base_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_project_path(self, project_path: Path) -> Path:
|
||||
"""Get the .fuzzforge storage path for a project.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Storage path (.fuzzforge inside project).
|
||||
|
||||
"""
|
||||
return project_path / FUZZFORGE_DIR_NAME
|
||||
|
||||
def init_project(self, project_path: Path) -> Path:
|
||||
"""Initialize storage for a new project.
|
||||
|
||||
Creates a .fuzzforge/ directory inside the project for storing
|
||||
configuration and execution results.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Path to the project storage directory.
|
||||
|
||||
"""
|
||||
storage_path = self._get_project_path(project_path)
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
(storage_path / "runs").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create .gitignore to avoid committing large files
|
||||
gitignore_path = storage_path / ".gitignore"
|
||||
if not gitignore_path.exists():
|
||||
gitignore_path.write_text(
|
||||
"# FuzzForge storage - ignore large/temporary files\n"
|
||||
"runs/\n"
|
||||
"!config.json\n"
|
||||
)
|
||||
|
||||
logger.info("Initialized project storage: %s", storage_path)
|
||||
return storage_path
|
||||
|
||||
def get_project_assets_path(self, project_path: Path) -> Path | None:
|
||||
"""Get the configured source path for a project.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Path to source directory, or None if not configured.
|
||||
|
||||
"""
|
||||
storage_path = self._get_project_path(project_path)
|
||||
config_path = storage_path / "config.json"
|
||||
|
||||
if config_path.exists():
|
||||
config = json.loads(config_path.read_text())
|
||||
source_path = config.get("source_path")
|
||||
if source_path:
|
||||
path = Path(source_path)
|
||||
if path.exists():
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
def set_project_assets(self, project_path: Path, assets_path: Path) -> Path:
|
||||
"""Set the source path for a project (reference only, no copying).
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:param assets_path: Path to source directory.
|
||||
:returns: The assets path (unchanged).
|
||||
:raises StorageError: If path doesn't exist.
|
||||
|
||||
"""
|
||||
if not assets_path.exists():
|
||||
msg = f"Assets path does not exist: {assets_path}"
|
||||
raise StorageError(msg)
|
||||
|
||||
assets_path = assets_path.resolve()
|
||||
|
||||
storage_path = self._get_project_path(project_path)
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
config_path = storage_path / "config.json"
|
||||
|
||||
config: dict = {}
|
||||
if config_path.exists():
|
||||
config = json.loads(config_path.read_text())
|
||||
|
||||
config["source_path"] = str(assets_path)
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
|
||||
logger.info("Set project assets: %s -> %s", project_path.name, assets_path)
|
||||
return assets_path
|
||||
|
||||
def list_executions(self, project_path: Path) -> list[str]:
|
||||
"""List all execution IDs for a project.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: List of execution IDs.
|
||||
|
||||
"""
|
||||
runs_dir = self._get_project_path(project_path) / "runs"
|
||||
if not runs_dir.exists():
|
||||
return []
|
||||
return [d.name for d in runs_dir.iterdir() if d.is_dir()]
|
||||
|
||||
def get_execution_results(
|
||||
self,
|
||||
project_path: Path,
|
||||
execution_id: str,
|
||||
) -> Path | None:
|
||||
"""Retrieve execution results path.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:param execution_id: Execution ID.
|
||||
:returns: Path to results archive, or None if not found.
|
||||
|
||||
"""
|
||||
storage_path = self._get_project_path(project_path)
|
||||
|
||||
# Try direct path
|
||||
results_path = storage_path / "runs" / execution_id / RESULTS_ARCHIVE_FILENAME
|
||||
if results_path.exists():
|
||||
return results_path
|
||||
|
||||
# Search in all run directories
|
||||
runs_dir = storage_path / "runs"
|
||||
if runs_dir.exists():
|
||||
for run_dir in runs_dir.iterdir():
|
||||
if run_dir.is_dir() and execution_id in run_dir.name:
|
||||
candidate = run_dir / RESULTS_ARCHIVE_FILENAME
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def extract_results(self, results_path: Path, destination: Path) -> Path:
|
||||
"""Extract a results archive to a destination directory.
|
||||
|
||||
:param results_path: Path to the results archive.
|
||||
:param destination: Directory to extract to.
|
||||
:returns: Path to extracted directory.
|
||||
:raises StorageError: If extraction fails.
|
||||
|
||||
"""
|
||||
try:
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
with Archive(results_path, "r:gz") as tar:
|
||||
tar.extractall(path=destination) # noqa: S202
|
||||
logger.info("Extracted results: %s -> %s", results_path, destination)
|
||||
return destination
|
||||
except Exception as exc:
|
||||
msg = f"Failed to extract results: {exc}"
|
||||
raise StorageError(msg) from exc
|
||||
@@ -1,149 +0,0 @@
|
||||
"""Project management tools for FuzzForge MCP."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.exceptions import ToolError
|
||||
|
||||
from fuzzforge_mcp.dependencies import get_project_path, get_storage, set_current_project_path
|
||||
|
||||
|
||||
mcp: FastMCP = FastMCP()
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def init_project(project_path: str | None = None) -> dict[str, Any]:
|
||||
"""Initialize a new FuzzForge project.
|
||||
|
||||
Creates a `.fuzzforge/` directory inside the project for storing:
|
||||
- config.json: Project configuration
|
||||
- runs/: Execution results
|
||||
|
||||
This should be called before executing hub tools.
|
||||
|
||||
:param project_path: Path to the project directory. If not provided, uses current directory.
|
||||
:return: Project initialization result.
|
||||
|
||||
"""
|
||||
storage = get_storage()
|
||||
|
||||
try:
|
||||
path = Path(project_path) if project_path else get_project_path()
|
||||
|
||||
# Track this as the current active project
|
||||
set_current_project_path(path)
|
||||
|
||||
storage_path = storage.init_project(path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"project_path": str(path),
|
||||
"storage_path": str(storage_path),
|
||||
"message": f"Project initialized. Storage at {path}/.fuzzforge/",
|
||||
}
|
||||
|
||||
except Exception as exception:
|
||||
message: str = f"Failed to initialize project: {exception}"
|
||||
raise ToolError(message) from exception
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def set_project_assets(assets_path: str) -> dict[str, Any]:
|
||||
"""Set the initial assets (source code) for a project.
|
||||
|
||||
This sets the DEFAULT source directory that will be mounted into
|
||||
hub tool containers via volume mounts.
|
||||
|
||||
:param assets_path: Path to the project source directory.
|
||||
:return: Result including stored assets path.
|
||||
|
||||
"""
|
||||
storage = get_storage()
|
||||
project_path: Path = get_project_path()
|
||||
|
||||
try:
|
||||
stored_path = storage.set_project_assets(
|
||||
project_path=project_path,
|
||||
assets_path=Path(assets_path),
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"project_path": str(project_path),
|
||||
"assets_path": str(stored_path),
|
||||
"message": f"Assets stored from {assets_path}",
|
||||
}
|
||||
|
||||
except Exception as exception:
|
||||
message: str = f"Failed to set project assets: {exception}"
|
||||
raise ToolError(message) from exception
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def list_executions() -> dict[str, Any]:
|
||||
"""List all executions for the current project.
|
||||
|
||||
Returns a list of execution IDs that can be used to retrieve results.
|
||||
|
||||
:return: List of execution IDs.
|
||||
|
||||
"""
|
||||
storage = get_storage()
|
||||
project_path: Path = get_project_path()
|
||||
|
||||
try:
|
||||
executions = storage.list_executions(project_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"project_path": str(project_path),
|
||||
"executions": executions,
|
||||
"count": len(executions),
|
||||
}
|
||||
|
||||
except Exception as exception:
|
||||
message: str = f"Failed to list executions: {exception}"
|
||||
raise ToolError(message) from exception
|
||||
|
||||
|
||||
@mcp.tool
|
||||
async def get_execution_results(execution_id: str, extract_to: str | None = None) -> dict[str, Any]:
|
||||
"""Get results for a specific execution.
|
||||
|
||||
:param execution_id: The execution ID to retrieve results for.
|
||||
:param extract_to: Optional directory to extract results to.
|
||||
:return: Result including path to results archive.
|
||||
|
||||
"""
|
||||
storage = get_storage()
|
||||
project_path: Path = get_project_path()
|
||||
|
||||
try:
|
||||
results_path = storage.get_execution_results(project_path, execution_id)
|
||||
|
||||
if results_path is None:
|
||||
return {
|
||||
"success": False,
|
||||
"execution_id": execution_id,
|
||||
"error": "Execution results not found",
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"success": True,
|
||||
"execution_id": execution_id,
|
||||
"results_path": str(results_path),
|
||||
}
|
||||
|
||||
# Extract if requested
|
||||
if extract_to:
|
||||
extracted_path = storage.extract_results(results_path, Path(extract_to))
|
||||
result["extracted_path"] = str(extracted_path)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as exception:
|
||||
message: str = f"Failed to get execution results: {exception}"
|
||||
raise ToolError(message) from exception
|
||||
@@ -1,3 +0,0 @@
|
||||
# fuzzforge-tests
|
||||
|
||||
Common test utilities and fixtures for FuzzForge packages.
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Common test utilities and fixtures for FuzzForge packages.
|
||||
|
||||
This package provides shared test utilities, fixtures, and helpers that can be
|
||||
reused across multiple FuzzForge packages to reduce code duplication and ensure
|
||||
consistency in testing approaches.
|
||||
|
||||
"""
|
||||
|
||||
__all__ = []
|
||||
565
hub-config.json
565
hub-config.json
@@ -1,105 +1,566 @@
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"name": "nmap-mcp",
|
||||
"description": "Network reconnaissance using Nmap - port scanning, service detection, OS fingerprinting",
|
||||
"name": "bloodhound-mcp",
|
||||
"description": "bloodhound-mcp \u2014 active-directory",
|
||||
"type": "docker",
|
||||
"image": "nmap-mcp:latest",
|
||||
"category": "reconnaissance",
|
||||
"capabilities": ["NET_RAW"],
|
||||
"enabled": true
|
||||
"image": "bloodhound-mcp:latest",
|
||||
"category": "active-directory",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "binwalk-mcp",
|
||||
"description": "Firmware extraction and analysis using Binwalk - file signatures, entropy analysis, embedded file extraction",
|
||||
"description": "binwalk-mcp \u2014 binary-analysis",
|
||||
"type": "docker",
|
||||
"image": "binwalk-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "yara-mcp",
|
||||
"description": "Pattern matching and malware classification using YARA rules",
|
||||
"type": "docker",
|
||||
"image": "yara-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "capa-mcp",
|
||||
"description": "Static capability detection using capa - identifies malware capabilities in binaries",
|
||||
"description": "capa-mcp \u2014 binary-analysis",
|
||||
"type": "docker",
|
||||
"image": "capa-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "ghidra-mcp",
|
||||
"description": "ghidra-mcp \u2014 binary-analysis",
|
||||
"type": "docker",
|
||||
"image": "ghidra-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "radare2-mcp",
|
||||
"description": "Binary analysis and reverse engineering using radare2",
|
||||
"description": "radare2-mcp \u2014 binary-analysis",
|
||||
"type": "docker",
|
||||
"image": "radare2-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "ghidra-mcp",
|
||||
"description": "Advanced binary decompilation and reverse engineering using Ghidra",
|
||||
"name": "yara-mcp",
|
||||
"description": "yara-mcp \u2014 binary-analysis",
|
||||
"type": "docker",
|
||||
"image": "ghcr.io/clearbluejar/pyghidra-mcp:latest",
|
||||
"image": "yara-mcp:latest",
|
||||
"category": "binary-analysis",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "searchsploit-mcp",
|
||||
"description": "CVE and exploit search using SearchSploit / Exploit-DB",
|
||||
"name": "daml-viewer-mcp",
|
||||
"description": "daml-viewer-mcp \u2014 blockchain",
|
||||
"type": "docker",
|
||||
"image": "searchsploit-mcp:latest",
|
||||
"category": "exploitation",
|
||||
"image": "daml-viewer-mcp:latest",
|
||||
"category": "blockchain",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "nuclei-mcp",
|
||||
"description": "Vulnerability scanning using Nuclei templates",
|
||||
"name": "medusa-mcp",
|
||||
"description": "medusa-mcp \u2014 blockchain",
|
||||
"type": "docker",
|
||||
"image": "nuclei-mcp:latest",
|
||||
"category": "web-security",
|
||||
"capabilities": ["NET_RAW"],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"image": "medusa-mcp:latest",
|
||||
"category": "blockchain",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "trivy-mcp",
|
||||
"description": "Container and filesystem vulnerability scanning using Trivy",
|
||||
"description": "trivy-mcp \u2014 cloud-security",
|
||||
"type": "docker",
|
||||
"image": "trivy-mcp:latest",
|
||||
"category": "cloud-security",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "searchsploit-mcp",
|
||||
"description": "searchsploit-mcp \u2014 exploitation",
|
||||
"type": "docker",
|
||||
"image": "searchsploit-mcp:latest",
|
||||
"category": "exploitation",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "nmap-mcp",
|
||||
"description": "nmap-mcp \u2014 reconnaissance",
|
||||
"type": "docker",
|
||||
"image": "nmap-mcp:latest",
|
||||
"category": "reconnaissance",
|
||||
"capabilities": [
|
||||
"NET_RAW"
|
||||
],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "gitleaks-mcp",
|
||||
"description": "Secret and credential detection in code and firmware using Gitleaks",
|
||||
"description": "gitleaks-mcp \u2014 secrets",
|
||||
"type": "docker",
|
||||
"image": "gitleaks-mcp:latest",
|
||||
"category": "secrets",
|
||||
"capabilities": [],
|
||||
"volumes": ["~/.fuzzforge/hub/workspace:/data"],
|
||||
"enabled": true
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "nuclei-mcp",
|
||||
"description": "nuclei-mcp \u2014 web-security",
|
||||
"type": "docker",
|
||||
"image": "nuclei-mcp:latest",
|
||||
"category": "web-security",
|
||||
"capabilities": [
|
||||
"NET_RAW"
|
||||
],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/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/.secpipe/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/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "go-analyzer-mcp",
|
||||
"description": "Go static analysis: fuzzable entry points, existing Fuzz* targets, unsafe/cgo usage, CVE scanning via govulncheck",
|
||||
"type": "docker",
|
||||
"image": "go-analyzer-mcp:latest",
|
||||
"category": "code-security",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "go-harness-tester-mcp",
|
||||
"description": "Test Go fuzz harness quality: compilation, seed execution, fuzzing trial, quality scoring 0-100",
|
||||
"type": "docker",
|
||||
"image": "go-harness-tester-mcp:latest",
|
||||
"category": "code-security",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "go-fuzzer-mcp",
|
||||
"description": "Run Go native fuzzing (go test -fuzz) with blocking and continuous modes, crash collection, session management",
|
||||
"type": "docker",
|
||||
"image": "go-fuzzer-mcp:latest",
|
||||
"category": "code-security",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
},
|
||||
{
|
||||
"name": "go-crash-analyzer-mcp",
|
||||
"description": "Analyze Go fuzzing crashes: reproduce, classify (nil-deref, OOR, panic, race, etc.), deduplicate by stack signature",
|
||||
"type": "docker",
|
||||
"image": "go-crash-analyzer-mcp:latest",
|
||||
"category": "code-security",
|
||||
"capabilities": [],
|
||||
"volumes": [
|
||||
"/home/afredefon/.secpipe/hub/workspace:/data"
|
||||
],
|
||||
"enabled": true,
|
||||
"source_hub": "mcp-security-hub"
|
||||
}
|
||||
],
|
||||
"default_timeout": 300,
|
||||
"cache_tools": true
|
||||
}
|
||||
"workflow_hints_file": "mcp-security-hub/workflow-hints.json"
|
||||
}
|
||||
@@ -1,33 +1,38 @@
|
||||
[project]
|
||||
name = "fuzzforge-oss"
|
||||
version = "1.0.0"
|
||||
description = "FuzzForge AI - AI-driven security research platform for local execution"
|
||||
name = "secpipe-oss"
|
||||
version = "0.8.0"
|
||||
description = "SecPipe AI - AI-driven security research platform for local execution"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
authors = [
|
||||
{ name = "FuzzingLabs", email = "contact@fuzzinglabs.com" }
|
||||
]
|
||||
dependencies = [
|
||||
"secpipe-cli",
|
||||
"secpipe-mcp",
|
||||
"secpipe-common",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest==9.0.2",
|
||||
"pytest-asyncio==1.3.0",
|
||||
"pytest-httpx==0.36.0",
|
||||
"fuzzforge-tests",
|
||||
"fuzzforge-common",
|
||||
"fuzzforge-mcp",
|
||||
"secpipe-tests",
|
||||
"secpipe-common",
|
||||
"secpipe-mcp",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"fuzzforge-common",
|
||||
"fuzzforge-mcp",
|
||||
"fuzzforge-cli",
|
||||
"fuzzforge-tests",
|
||||
"secpipe-common",
|
||||
"secpipe-mcp",
|
||||
"secpipe-cli",
|
||||
"secpipe-tests",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
fuzzforge-common = { workspace = true }
|
||||
fuzzforge-mcp = { workspace = true }
|
||||
fuzzforge-cli = { workspace = true }
|
||||
fuzzforge-tests = { workspace = true }
|
||||
secpipe-common = { workspace = true }
|
||||
secpipe-mcp = { workspace = true }
|
||||
secpipe-cli = { workspace = true }
|
||||
secpipe-tests = { workspace = true }
|
||||
|
||||
3
secpipe-cli/README.md
Normal file
3
secpipe-cli/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# SecPipe CLI
|
||||
|
||||
...
|
||||
@@ -1,13 +1,14 @@
|
||||
[project]
|
||||
name = "fuzzforge-cli"
|
||||
name = "secpipe-cli"
|
||||
version = "0.0.1"
|
||||
description = "FuzzForge CLI - Command-line interface for FuzzForge AI."
|
||||
description = "SecPipe CLI - Command-line interface for SecPipe AI."
|
||||
authors = []
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"fuzzforge-mcp==0.0.1",
|
||||
"secpipe-mcp==0.0.1",
|
||||
"rich>=14.0.0",
|
||||
"textual>=1.0.0",
|
||||
"typer==0.20.1",
|
||||
]
|
||||
|
||||
@@ -22,7 +23,7 @@ tests = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
fuzzforge = "fuzzforge_cli.__main__:main"
|
||||
secpipe = "secpipe_cli.__main__:main"
|
||||
|
||||
[tool.uv.sources]
|
||||
fuzzforge-mcp = { workspace = true }
|
||||
secpipe-mcp = { workspace = true }
|
||||
61
secpipe-cli/ruff.toml
Normal file
61
secpipe-cli/ruff.toml
Normal file
@@ -0,0 +1,61 @@
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [ "ALL" ]
|
||||
ignore = [
|
||||
"COM812", # conflicts with the formatter
|
||||
"D203", # conflicts with 'D211'
|
||||
"D213", # conflicts with 'D212'
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"PLR2004", # allowing comparisons using unamed numerical constants in tests
|
||||
"S101", # allowing 'assert' statements in tests
|
||||
]
|
||||
"src/secpipe_cli/tui/**" = [
|
||||
"ARG002", # unused method argument: callback signature
|
||||
"BLE001", # blind exception: broad error handling in UI
|
||||
"C901", # complexity: UI logic
|
||||
"D107", # missing docstring in __init__: simple dataclasses
|
||||
"FBT001", # boolean positional arg
|
||||
"FBT002", # boolean default arg
|
||||
"PLC0415", # import outside top-level: lazy loading
|
||||
"PLR0911", # too many return statements
|
||||
"PLR0912", # too many branches
|
||||
"PLR2004", # magic value comparison
|
||||
"RUF012", # mutable class default: Textual pattern
|
||||
"S603", # subprocess: validated inputs
|
||||
"S607", # subprocess: PATH lookup
|
||||
"SIM108", # ternary: readability preference
|
||||
"TC001", # TYPE_CHECKING: runtime type needs
|
||||
"TC002", # TYPE_CHECKING: runtime type needs
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
"TRY300", # try-else: existing pattern
|
||||
]
|
||||
"tui/*.py" = [
|
||||
"D107", # missing docstring in __init__: simple dataclasses
|
||||
"TC001", # TYPE_CHECKING: runtime type needs
|
||||
"TC002", # TYPE_CHECKING: runtime type needs
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
]
|
||||
"src/secpipe_cli/commands/mcp.py" = [
|
||||
"ARG001", # unused argument: callback signature
|
||||
"B904", # raise from: existing pattern
|
||||
"F841", # unused variable: legacy code
|
||||
"FBT002", # boolean default arg
|
||||
"PLR0912", # too many branches
|
||||
"PLR0915", # too many statements
|
||||
"SIM108", # ternary: readability preference
|
||||
]
|
||||
"src/secpipe_cli/application.py" = [
|
||||
"B008", # function call in default: Path.cwd()
|
||||
"PLC0415", # import outside top-level: lazy loading
|
||||
]
|
||||
"src/secpipe_cli/commands/projects.py" = [
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
]
|
||||
"src/secpipe_cli/context.py" = [
|
||||
"TC002", # TYPE_CHECKING: runtime type needs
|
||||
"TC003", # TYPE_CHECKING: runtime type needs
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
"""TODO."""
|
||||
|
||||
from fuzzforge_cli.application import application
|
||||
from secpipe_cli.application import application
|
||||
|
||||
|
||||
def main() -> None:
|
||||
70
secpipe-cli/src/secpipe_cli/application.py
Normal file
70
secpipe-cli/src/secpipe_cli/application.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""SecPipe CLI application."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from secpipe_mcp.storage import LocalStorage # type: ignore[import-untyped]
|
||||
from typer import Context as TyperContext
|
||||
from typer import Option, Typer
|
||||
|
||||
from secpipe_cli.commands import mcp, projects
|
||||
from secpipe_cli.context import Context
|
||||
|
||||
application: Typer = Typer(
|
||||
name="secpipe",
|
||||
help="SecPipe AI - Security research orchestration platform.",
|
||||
)
|
||||
|
||||
|
||||
@application.callback()
|
||||
def main(
|
||||
project_path: Annotated[
|
||||
Path,
|
||||
Option(
|
||||
"--project",
|
||||
"-p",
|
||||
envvar="SECPIPE_PROJECT__DEFAULT_PATH",
|
||||
help="Path to the SecPipe project directory.",
|
||||
),
|
||||
] = Path.cwd(),
|
||||
storage_path: Annotated[
|
||||
Path,
|
||||
Option(
|
||||
"--storage",
|
||||
envvar="SECPIPE_STORAGE__PATH",
|
||||
help="Path to the storage directory.",
|
||||
),
|
||||
] = Path.cwd() / ".secpipe" / "storage",
|
||||
context: TyperContext = None, # type: ignore[assignment]
|
||||
) -> None:
|
||||
"""SecPipe AI - Security research orchestration platform.
|
||||
|
||||
Discover and execute MCP hub tools for security research.
|
||||
|
||||
"""
|
||||
storage = LocalStorage(base_path=storage_path)
|
||||
|
||||
context.obj = Context(
|
||||
storage=storage,
|
||||
project_path=project_path,
|
||||
)
|
||||
|
||||
|
||||
application.add_typer(mcp.application)
|
||||
application.add_typer(projects.application)
|
||||
|
||||
|
||||
@application.command(
|
||||
name="ui",
|
||||
help="Launch the SecPipe terminal interface.",
|
||||
)
|
||||
def launch_ui() -> None:
|
||||
"""Launch the interactive SecPipe TUI dashboard.
|
||||
|
||||
Provides a visual dashboard showing AI agent connection status
|
||||
and hub server availability, with wizards for setup and configuration.
|
||||
|
||||
"""
|
||||
from secpipe_cli.tui.app import SecPipeApp
|
||||
|
||||
SecPipeApp().run()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""MCP server configuration commands for FuzzForge CLI.
|
||||
"""MCP server configuration commands for SecPipe CLI.
|
||||
|
||||
This module provides commands for setting up MCP server connections
|
||||
with various AI agents (VS Code Copilot, Claude Code, etc.).
|
||||
@@ -12,7 +12,7 @@ import os
|
||||
import sys
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
@@ -44,10 +44,10 @@ def _get_copilot_mcp_path() -> Path:
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
return Path.home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json"
|
||||
elif sys.platform == "win32":
|
||||
if sys.platform == "win32":
|
||||
return Path(os.environ.get("APPDATA", "")) / "Code" / "User" / "mcp.json"
|
||||
else: # Linux
|
||||
return Path.home() / ".config" / "Code" / "User" / "mcp.json"
|
||||
# Linux
|
||||
return Path.home() / ".config" / "Code" / "User" / "mcp.json"
|
||||
|
||||
|
||||
def _get_claude_desktop_mcp_path() -> Path:
|
||||
@@ -58,10 +58,10 @@ def _get_claude_desktop_mcp_path() -> Path:
|
||||
"""
|
||||
if sys.platform == "darwin":
|
||||
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
|
||||
elif sys.platform == "win32":
|
||||
if sys.platform == "win32":
|
||||
return Path(os.environ.get("APPDATA", "")) / "Claude" / "claude_desktop_config.json"
|
||||
else: # Linux
|
||||
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
||||
# Linux
|
||||
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
||||
|
||||
|
||||
def _get_claude_code_mcp_path(project_path: Path | None = None) -> Path:
|
||||
@@ -114,76 +114,86 @@ def _detect_docker_socket() -> str:
|
||||
:returns: Path to the Docker socket.
|
||||
|
||||
"""
|
||||
socket_paths = [
|
||||
"/var/run/docker.sock",
|
||||
socket_paths: list[Path] = [
|
||||
Path("/var/run/docker.sock"),
|
||||
Path.home() / ".docker" / "run" / "docker.sock",
|
||||
]
|
||||
|
||||
for path in socket_paths:
|
||||
if Path(path).exists():
|
||||
if path.exists():
|
||||
return str(path)
|
||||
|
||||
return "/var/run/docker.sock"
|
||||
|
||||
|
||||
def _find_fuzzforge_root() -> Path:
|
||||
"""Find the FuzzForge installation root.
|
||||
def _find_secpipe_root() -> Path:
|
||||
"""Find the SecPipe installation root.
|
||||
|
||||
:returns: Path to fuzzforge-oss directory.
|
||||
:returns: Path to secpipe-oss directory.
|
||||
|
||||
"""
|
||||
# Try to find from current file location
|
||||
current = Path(__file__).resolve()
|
||||
# Check environment variable override first
|
||||
env_root = os.environ.get("SECPIPE_ROOT")
|
||||
if env_root:
|
||||
return Path(env_root).resolve()
|
||||
|
||||
# Walk up to find fuzzforge-oss root
|
||||
for parent in current.parents:
|
||||
if (parent / "fuzzforge-mcp").is_dir():
|
||||
# Walk up from cwd to find a secpipe root (hub-config.json is the marker)
|
||||
for parent in [Path.cwd(), *Path.cwd().parents]:
|
||||
if (parent / "hub-config.json").is_file():
|
||||
return parent
|
||||
|
||||
# Fall back to __file__-based search (dev install inside secpipe-oss)
|
||||
current = Path(__file__).resolve()
|
||||
for parent in current.parents:
|
||||
if (parent / "secpipe-mcp").is_dir():
|
||||
return parent
|
||||
|
||||
# Fall back to cwd
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def _generate_mcp_config(
|
||||
fuzzforge_root: Path,
|
||||
secpipe_root: Path,
|
||||
engine_type: str,
|
||||
engine_socket: str,
|
||||
) -> dict:
|
||||
) -> dict[str, Any]:
|
||||
"""Generate MCP server configuration.
|
||||
|
||||
:param fuzzforge_root: Path to fuzzforge-oss installation.
|
||||
:param secpipe_root: Path to secpipe-oss installation.
|
||||
:param engine_type: Container engine type (podman or docker).
|
||||
:param engine_socket: Container engine socket path.
|
||||
:returns: MCP configuration dictionary.
|
||||
|
||||
"""
|
||||
venv_python = fuzzforge_root / ".venv" / "bin" / "python"
|
||||
venv_python = secpipe_root / ".venv" / "bin" / "python"
|
||||
|
||||
# Use uv run if no venv, otherwise use venv python directly
|
||||
if venv_python.exists():
|
||||
command = str(venv_python)
|
||||
args = ["-m", "fuzzforge_mcp"]
|
||||
args = ["-m", "secpipe_mcp"]
|
||||
else:
|
||||
command = "uv"
|
||||
args = ["--directory", str(fuzzforge_root), "run", "fuzzforge-mcp"]
|
||||
args = ["--directory", str(secpipe_root), "run", "secpipe-mcp"]
|
||||
|
||||
# Self-contained storage paths for FuzzForge containers
|
||||
# This isolates FuzzForge from system Podman and avoids snap issues
|
||||
fuzzforge_home = Path.home() / ".fuzzforge"
|
||||
graphroot = fuzzforge_home / "containers" / "storage"
|
||||
runroot = fuzzforge_home / "containers" / "run"
|
||||
# User-global storage paths for SecPipe containers.
|
||||
# Kept under ~/.secpipe so images are built once and shared across
|
||||
# all workspaces — regardless of where `secpipe mcp install` is run.
|
||||
# Override with SECPIPE_USER_DIR for isolated testing.
|
||||
user_dir_env = os.environ.get("SECPIPE_USER_DIR")
|
||||
secpipe_home = Path(user_dir_env).resolve() if user_dir_env else Path.home() / ".secpipe"
|
||||
graphroot = secpipe_home / "containers" / "storage"
|
||||
runroot = secpipe_home / "containers" / "run"
|
||||
|
||||
return {
|
||||
"type": "stdio",
|
||||
"command": command,
|
||||
"args": args,
|
||||
"cwd": str(fuzzforge_root),
|
||||
"cwd": str(secpipe_root),
|
||||
"env": {
|
||||
"FUZZFORGE_ENGINE__TYPE": engine_type,
|
||||
"FUZZFORGE_ENGINE__GRAPHROOT": str(graphroot),
|
||||
"FUZZFORGE_ENGINE__RUNROOT": str(runroot),
|
||||
"FUZZFORGE_HUB__ENABLED": "true",
|
||||
"FUZZFORGE_HUB__CONFIG_PATH": str(fuzzforge_root / "hub-config.json"),
|
||||
"SECPIPE_ENGINE__TYPE": engine_type,
|
||||
"SECPIPE_ENGINE__GRAPHROOT": str(graphroot),
|
||||
"SECPIPE_ENGINE__RUNROOT": str(runroot),
|
||||
"SECPIPE_HUB__ENABLED": "true",
|
||||
"SECPIPE_HUB__CONFIG_PATH": str(secpipe_root / "hub-config.json"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -204,9 +214,9 @@ def status(context: Context) -> None:
|
||||
table.add_column("Agent", style="cyan")
|
||||
table.add_column("Config Path")
|
||||
table.add_column("Status")
|
||||
table.add_column("FuzzForge Configured")
|
||||
table.add_column("SecPipe Configured")
|
||||
|
||||
fuzzforge_root = _find_fuzzforge_root()
|
||||
secpipe_root = _find_secpipe_root()
|
||||
|
||||
agents = [
|
||||
("GitHub Copilot", _get_copilot_mcp_path(), "servers"),
|
||||
@@ -219,12 +229,12 @@ def status(context: Context) -> None:
|
||||
try:
|
||||
config = json.loads(config_path.read_text())
|
||||
servers = config.get(servers_key, {})
|
||||
has_fuzzforge = "fuzzforge" in servers
|
||||
has_secpipe = "secpipe" in servers
|
||||
table.add_row(
|
||||
name,
|
||||
str(config_path),
|
||||
"[green]✓ Exists[/green]",
|
||||
"[green]✓ Yes[/green]" if has_fuzzforge else "[yellow]✗ No[/yellow]",
|
||||
"[green]✓ Yes[/green]" if has_secpipe else "[yellow]✗ No[/yellow]",
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
table.add_row(
|
||||
@@ -246,7 +256,7 @@ def status(context: Context) -> None:
|
||||
# Show detected environment
|
||||
console.print()
|
||||
console.print("[bold]Detected Environment:[/bold]")
|
||||
console.print(f" FuzzForge Root: {_find_fuzzforge_root()}")
|
||||
console.print(f" SecPipe Root: {_find_secpipe_root()}")
|
||||
console.print(f" Podman Socket: {_detect_podman_socket()}")
|
||||
console.print(f" Docker Socket: {_detect_docker_socket()}")
|
||||
|
||||
@@ -280,7 +290,7 @@ def generate(
|
||||
|
||||
"""
|
||||
console = Console()
|
||||
fuzzforge_root = _find_fuzzforge_root()
|
||||
secpipe_root = _find_secpipe_root()
|
||||
|
||||
# Detect socket
|
||||
if engine == "podman":
|
||||
@@ -290,16 +300,16 @@ def generate(
|
||||
|
||||
# Generate config
|
||||
server_config = _generate_mcp_config(
|
||||
fuzzforge_root=fuzzforge_root,
|
||||
secpipe_root=secpipe_root,
|
||||
engine_type=engine,
|
||||
engine_socket=socket,
|
||||
)
|
||||
|
||||
# Format based on agent
|
||||
if agent == AIAgent.COPILOT:
|
||||
full_config = {"servers": {"fuzzforge": server_config}}
|
||||
full_config = {"servers": {"secpipe": server_config}}
|
||||
else: # Claude Desktop or Claude Code
|
||||
full_config = {"mcpServers": {"fuzzforge": server_config}}
|
||||
full_config = {"mcpServers": {"secpipe": server_config}}
|
||||
|
||||
config_json = json.dumps(full_config, indent=4)
|
||||
|
||||
@@ -312,14 +322,14 @@ def generate(
|
||||
if agent == AIAgent.COPILOT:
|
||||
config_path = _get_copilot_mcp_path()
|
||||
elif agent == AIAgent.CLAUDE_CODE:
|
||||
config_path = _get_claude_code_mcp_path(fuzzforge_root)
|
||||
config_path = _get_claude_code_mcp_path(secpipe_root)
|
||||
else: # Claude Desktop
|
||||
config_path = _get_claude_desktop_mcp_path()
|
||||
|
||||
console.print()
|
||||
console.print(f"[bold]Save to:[/bold] {config_path}")
|
||||
console.print()
|
||||
console.print("[dim]Or run 'fuzzforge mcp install' to install automatically.[/dim]")
|
||||
console.print("[dim]Or run 'secpipe mcp install' to install automatically.[/dim]")
|
||||
|
||||
|
||||
@application.command(
|
||||
@@ -347,14 +357,14 @@ def install(
|
||||
Option(
|
||||
"--force",
|
||||
"-f",
|
||||
help="Overwrite existing fuzzforge configuration.",
|
||||
help="Overwrite existing secpipe configuration.",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Install MCP configuration for the specified AI agent.
|
||||
|
||||
This will create or update the MCP configuration file, adding the
|
||||
fuzzforge server configuration.
|
||||
secpipe server configuration.
|
||||
|
||||
:param context: Typer context.
|
||||
:param agent: Target AI agent.
|
||||
@@ -363,7 +373,7 @@ def install(
|
||||
|
||||
"""
|
||||
console = Console()
|
||||
fuzzforge_root = _find_fuzzforge_root()
|
||||
secpipe_root = _find_secpipe_root()
|
||||
|
||||
# Determine config path
|
||||
if agent == AIAgent.COPILOT:
|
||||
@@ -384,7 +394,7 @@ def install(
|
||||
|
||||
# Generate server config
|
||||
server_config = _generate_mcp_config(
|
||||
fuzzforge_root=fuzzforge_root,
|
||||
secpipe_root=secpipe_root,
|
||||
engine_type=engine,
|
||||
engine_socket=socket,
|
||||
)
|
||||
@@ -398,47 +408,47 @@ def install(
|
||||
console.print("[dim]Please fix the file manually or delete it.[/dim]")
|
||||
raise SystemExit(1)
|
||||
|
||||
# Check if fuzzforge already exists
|
||||
# Check if secpipe already exists
|
||||
servers = existing_config.get(servers_key, {})
|
||||
if "fuzzforge" in servers and not force:
|
||||
console.print("[yellow]FuzzForge is already configured.[/yellow]")
|
||||
if "secpipe" in servers and not force:
|
||||
console.print("[yellow]SecPipe is already configured.[/yellow]")
|
||||
console.print("[dim]Use --force to overwrite existing configuration.[/dim]")
|
||||
raise SystemExit(1)
|
||||
|
||||
# Add/update fuzzforge
|
||||
# Add/update secpipe
|
||||
if servers_key not in existing_config:
|
||||
existing_config[servers_key] = {}
|
||||
existing_config[servers_key]["fuzzforge"] = server_config
|
||||
existing_config[servers_key]["secpipe"] = server_config
|
||||
|
||||
full_config = existing_config
|
||||
else:
|
||||
# Create new config
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
full_config = {servers_key: {"fuzzforge": server_config}}
|
||||
full_config = {servers_key: {"secpipe": server_config}}
|
||||
|
||||
# Write config
|
||||
config_path.write_text(json.dumps(full_config, indent=4))
|
||||
|
||||
console.print(f"[green]✓ Installed FuzzForge MCP configuration for {agent.value}[/green]")
|
||||
console.print(f"[green]✓ Installed SecPipe MCP configuration for {agent.value}[/green]")
|
||||
console.print()
|
||||
console.print(f"[bold]Configuration file:[/bold] {config_path}")
|
||||
console.print()
|
||||
console.print("[bold]Settings:[/bold]")
|
||||
console.print(f" Engine: {engine}")
|
||||
console.print(f" Socket: {socket}")
|
||||
console.print(f" Hub Config: {fuzzforge_root / 'hub-config.json'}")
|
||||
console.print(f" Hub Config: {secpipe_root / 'hub-config.json'}")
|
||||
console.print()
|
||||
|
||||
console.print("[bold]Next steps:[/bold]")
|
||||
if agent == AIAgent.COPILOT:
|
||||
console.print(" 1. Restart VS Code")
|
||||
console.print(" 2. Open Copilot Chat and look for FuzzForge tools")
|
||||
console.print(" 2. Open Copilot Chat and look for SecPipe tools")
|
||||
elif agent == AIAgent.CLAUDE_CODE:
|
||||
console.print(" 1. Run 'claude' from any directory")
|
||||
console.print(" 2. FuzzForge tools will be available")
|
||||
console.print(" 2. SecPipe tools will be available")
|
||||
else: # Claude Desktop
|
||||
console.print(" 1. Restart Claude Desktop")
|
||||
console.print(" 2. The fuzzforge MCP server will be available")
|
||||
console.print(" 2. The secpipe MCP server will be available")
|
||||
|
||||
|
||||
@application.command(
|
||||
@@ -454,14 +464,14 @@ def uninstall(
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
"""Remove FuzzForge MCP configuration from the specified AI agent.
|
||||
"""Remove SecPipe MCP configuration from the specified AI agent.
|
||||
|
||||
:param context: Typer context.
|
||||
:param agent: Target AI agent.
|
||||
|
||||
"""
|
||||
console = Console()
|
||||
fuzzforge_root = _find_fuzzforge_root()
|
||||
secpipe_root = _find_secpipe_root()
|
||||
|
||||
# Determine config path
|
||||
if agent == AIAgent.COPILOT:
|
||||
@@ -485,16 +495,16 @@ def uninstall(
|
||||
raise SystemExit(1)
|
||||
|
||||
servers = config.get(servers_key, {})
|
||||
if "fuzzforge" not in servers:
|
||||
console.print("[yellow]FuzzForge is not configured.[/yellow]")
|
||||
if "secpipe" not in servers:
|
||||
console.print("[yellow]SecPipe is not configured.[/yellow]")
|
||||
return
|
||||
|
||||
# Remove fuzzforge
|
||||
del servers["fuzzforge"]
|
||||
# Remove secpipe
|
||||
del servers["secpipe"]
|
||||
|
||||
# Write back
|
||||
config_path.write_text(json.dumps(config, indent=4))
|
||||
|
||||
console.print(f"[green]✓ Removed FuzzForge MCP configuration from {agent.value}[/green]")
|
||||
console.print(f"[green]✓ Removed SecPipe MCP configuration from {agent.value}[/green]")
|
||||
console.print()
|
||||
console.print("[dim]Restart your AI agent for changes to take effect.[/dim]")
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Project management commands for FuzzForge CLI."""
|
||||
"""Project management commands for SecPipe CLI."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
@@ -7,7 +7,7 @@ from rich.console import Console
|
||||
from rich.table import Table
|
||||
from typer import Argument, Context, Option, Typer
|
||||
|
||||
from fuzzforge_cli.context import get_project_path, get_storage
|
||||
from secpipe_cli.context import get_project_path, get_storage
|
||||
|
||||
application: Typer = Typer(
|
||||
name="project",
|
||||
@@ -16,7 +16,7 @@ application: Typer = Typer(
|
||||
|
||||
|
||||
@application.command(
|
||||
help="Initialize a new FuzzForge project.",
|
||||
help="Initialize a new SecPipe project.",
|
||||
name="init",
|
||||
)
|
||||
def init_project(
|
||||
@@ -28,7 +28,7 @@ def init_project(
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Initialize a new FuzzForge project.
|
||||
"""Initialize a new SecPipe project.
|
||||
|
||||
Creates the necessary storage directories for the project.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""FuzzForge CLI context management."""
|
||||
"""SecPipe CLI context management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from fuzzforge_mcp.storage import LocalStorage
|
||||
from secpipe_mcp.storage import LocalStorage # type: ignore[import-untyped]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typer import Context as TyperContext
|
||||
@@ -20,7 +20,7 @@ class Context:
|
||||
def __init__(self, storage: LocalStorage, project_path: Path) -> None:
|
||||
"""Initialize an instance of the class.
|
||||
|
||||
:param storage: FuzzForge local storage instance.
|
||||
:param storage: SecPipe local storage instance.
|
||||
:param project_path: Path to the current project.
|
||||
|
||||
"""
|
||||
1
secpipe-cli/src/secpipe_cli/tui/__init__.py
Normal file
1
secpipe-cli/src/secpipe_cli/tui/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""SecPipe terminal user interface."""
|
||||
562
secpipe-cli/src/secpipe_cli/tui/app.py
Normal file
562
secpipe-cli/src/secpipe_cli/tui/app.py
Normal file
@@ -0,0 +1,562 @@
|
||||
"""SecPipe TUI application.
|
||||
|
||||
Main terminal user interface for SecPipe, providing a dashboard
|
||||
with AI agent connection status, hub server availability, and
|
||||
hub management capabilities.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from rich.text import Text
|
||||
from textual import events, work
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||||
from textual.message import Message
|
||||
from textual.widgets import Button, DataTable, Footer, Header
|
||||
|
||||
from secpipe_cli.tui.helpers import (
|
||||
check_agent_status,
|
||||
check_hub_image,
|
||||
find_secpipe_root,
|
||||
get_agent_configs,
|
||||
load_hub_config,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from secpipe_cli.commands.mcp import AIAgent
|
||||
|
||||
# Agent config entries stored alongside their linked status for row mapping
|
||||
_AgentRow = tuple[str, "AIAgent", Path, str, bool]
|
||||
|
||||
|
||||
class SingleClickDataTable(DataTable[Any]):
|
||||
"""DataTable subclass that also fires ``RowClicked`` on a single mouse click.
|
||||
|
||||
Textual's built-in ``RowSelected`` only fires on Enter or on a second click
|
||||
of an already-highlighted row. ``RowClicked`` fires on every first click,
|
||||
enabling single-click-to-act UX without requiring Enter.
|
||||
"""
|
||||
|
||||
class RowClicked(Message):
|
||||
"""Fired on every single mouse click on a data row."""
|
||||
|
||||
def __init__(self, data_table: SingleClickDataTable, cursor_row: int) -> None:
|
||||
self.data_table = data_table
|
||||
self.cursor_row = cursor_row
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def control(self) -> SingleClickDataTable:
|
||||
"""Return the data table that fired this event."""
|
||||
return self.data_table
|
||||
|
||||
async def _on_click(self, event: events.Click) -> None:
|
||||
"""Forward to parent, then post RowClicked on every mouse click.
|
||||
|
||||
The hub table is handled exclusively via RowClicked. RowSelected is
|
||||
intentionally NOT used for the hub table to avoid double-dispatch.
|
||||
"""
|
||||
await super()._on_click(event)
|
||||
meta = event.style.meta
|
||||
if meta and "row" in meta and self.cursor_type == "row":
|
||||
row_index: int = int(meta["row"])
|
||||
if row_index >= 0:
|
||||
self.post_message(SingleClickDataTable.RowClicked(self, row_index))
|
||||
|
||||
|
||||
class SecPipeApp(App[None]):
|
||||
"""SecPipe AI terminal user interface."""
|
||||
|
||||
TITLE = "SecPipe 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,
|
||||
BuildImageScreen, BuildLogScreen {
|
||||
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;
|
||||
}
|
||||
|
||||
#build-dialog {
|
||||
width: 72;
|
||||
height: auto;
|
||||
max-height: 80%;
|
||||
border: thick #4699fc;
|
||||
background: $surface;
|
||||
padding: 2 3;
|
||||
}
|
||||
|
||||
#confirm-text {
|
||||
margin: 1 0 2 0;
|
||||
}
|
||||
|
||||
#build-log {
|
||||
height: 30;
|
||||
border: round $panel;
|
||||
margin: 1 0;
|
||||
}
|
||||
|
||||
#build-subtitle {
|
||||
color: $text-muted;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#build-status {
|
||||
height: 1;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
.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"),
|
||||
Binding("enter", "select_row", "Select", show=False),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the dashboard layout."""
|
||||
yield Header()
|
||||
with VerticalScroll(id="main"):
|
||||
with Vertical(id="hub-panel", classes="panel"):
|
||||
yield SingleClickDataTable(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._hub_rows: list[tuple[str, str, str, bool] | None] = []
|
||||
# Background build tracking
|
||||
self._active_builds: dict[str, object] = {} # image -> Popen
|
||||
self._build_logs: dict[str, list[str]] = {} # image -> log lines
|
||||
self._build_results: dict[str, bool] = {} # image -> success
|
||||
self.query_one("#hub-panel").border_title = "Hub Servers [dim](click ✗ Not built to build)[/dim]"
|
||||
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."""
|
||||
self._hub_rows = []
|
||||
table = self.query_one("#hub-table", SingleClickDataTable)
|
||||
table.clear(columns=True)
|
||||
table.add_columns("Server", "Image", "Hub", "Status")
|
||||
table.cursor_type = "row"
|
||||
|
||||
try:
|
||||
secpipe_root = find_secpipe_root()
|
||||
hub_config = load_hub_config(secpipe_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[str, Any]]] = 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[str, Any], 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, "", "", "")
|
||||
self._hub_rows.append(None) # group header — not selectable
|
||||
|
||||
# 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 image in getattr(self, "_active_builds", {}):
|
||||
status_cell = Text("⏳ Building…", style="yellow")
|
||||
elif 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 dim")
|
||||
|
||||
table.add_row(
|
||||
f" {name}",
|
||||
Text(image, style="dim"),
|
||||
hub_name,
|
||||
status_cell,
|
||||
)
|
||||
self._hub_rows.append((name, image, hub_name, is_ready))
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Handle Enter-key row selection (agents table only).
|
||||
|
||||
Hub table uses RowClicked exclusively — wiring it to RowSelected too
|
||||
would cause a double push on every click since Textual 8 fires
|
||||
RowSelected on ALL clicks, not just second-click-on-same-row.
|
||||
"""
|
||||
if event.data_table.id == "agents-table":
|
||||
self._handle_agent_row(event.cursor_row)
|
||||
|
||||
def on_single_click_data_table_row_clicked(
|
||||
self, event: SingleClickDataTable.RowClicked
|
||||
) -> None:
|
||||
"""Handle single mouse-click on a hub table row."""
|
||||
if event.data_table.id == "hub-table":
|
||||
self._handle_hub_row(event.cursor_row)
|
||||
|
||||
def _handle_agent_row(self, idx: int) -> None:
|
||||
"""Open agent setup/unlink for the selected agent 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 secpipe_cli.tui.screens.agent_setup import AgentUnlinkScreen
|
||||
|
||||
self.push_screen(
|
||||
AgentUnlinkScreen(agent, display_name),
|
||||
callback=self._on_agent_changed,
|
||||
)
|
||||
else:
|
||||
from secpipe_cli.tui.screens.agent_setup import AgentSetupScreen
|
||||
|
||||
self.push_screen(
|
||||
AgentSetupScreen(agent, display_name),
|
||||
callback=self._on_agent_changed,
|
||||
)
|
||||
|
||||
def _handle_hub_row(self, idx: int) -> None:
|
||||
"""Handle a click on a hub table row."""
|
||||
# Guard: never push two build dialogs at once (double-click protection)
|
||||
if getattr(self, "_build_dialog_open", False):
|
||||
return
|
||||
|
||||
if idx < 0 or idx >= len(self._hub_rows):
|
||||
return
|
||||
row_data = self._hub_rows[idx]
|
||||
if row_data is None:
|
||||
return # group header row — ignore
|
||||
|
||||
server_name, image, hub_name, is_ready = row_data
|
||||
|
||||
# If a build is already running, open the live log viewer
|
||||
if image in self._active_builds:
|
||||
from secpipe_cli.tui.screens.build_log import BuildLogScreen
|
||||
self._build_dialog_open = True
|
||||
self.push_screen(
|
||||
BuildLogScreen(image),
|
||||
callback=lambda _: setattr(self, "_build_dialog_open", False),
|
||||
)
|
||||
return
|
||||
|
||||
if is_ready:
|
||||
self.notify(f"{image} is already built ✓", severity="information")
|
||||
return
|
||||
|
||||
if hub_name == "manual":
|
||||
self.notify("Manual servers must be built outside SecPipe")
|
||||
return
|
||||
|
||||
from secpipe_cli.tui.screens.build_image import BuildImageScreen
|
||||
|
||||
self._build_dialog_open = True
|
||||
|
||||
def _on_build_dialog_done(result: bool | None) -> None:
|
||||
self._build_dialog_open = False
|
||||
if result is not None:
|
||||
self._on_build_confirmed(result, server_name, image, hub_name)
|
||||
|
||||
self.push_screen(
|
||||
BuildImageScreen(server_name, image, hub_name),
|
||||
callback=_on_build_dialog_done,
|
||||
)
|
||||
|
||||
def _on_build_confirmed(self, confirmed: bool, server_name: str, image: str, hub_name: str) -> None:
|
||||
"""Start a background build if the user confirmed."""
|
||||
if not confirmed:
|
||||
return
|
||||
self._build_logs[image] = []
|
||||
self._build_results.pop(image, None)
|
||||
self._active_builds[image] = True # mark as pending so ⏳ shows immediately
|
||||
self._refresh_hub() # show ⏳ Building… immediately
|
||||
self._run_build(server_name, image, hub_name)
|
||||
|
||||
@work(thread=True)
|
||||
def _run_build(self, server_name: str, image: str, hub_name: str) -> None:
|
||||
"""Build a Docker/Podman image in a background thread."""
|
||||
from secpipe_cli.tui.helpers import build_image, find_dockerfile_for_server
|
||||
|
||||
logs = self._build_logs.setdefault(image, [])
|
||||
|
||||
dockerfile = find_dockerfile_for_server(server_name, hub_name)
|
||||
if dockerfile is None:
|
||||
logs.append(f"ERROR: Dockerfile not found for '{server_name}' in hub '{hub_name}'")
|
||||
self._build_results[image] = False
|
||||
self._active_builds.pop(image, None)
|
||||
self.call_from_thread(self._on_build_done, image, success=False)
|
||||
return
|
||||
|
||||
logs.append(f"Building {image} from {dockerfile.parent}")
|
||||
logs.append("")
|
||||
|
||||
try:
|
||||
proc = build_image(image, dockerfile)
|
||||
except FileNotFoundError as exc:
|
||||
logs.append(f"ERROR: {exc}")
|
||||
self._build_results[image] = False
|
||||
self._active_builds.pop(image, None)
|
||||
self.call_from_thread(self._on_build_done, image, success=False)
|
||||
return
|
||||
|
||||
self._active_builds[image] = proc # replace pending marker with actual process
|
||||
self.call_from_thread(self._refresh_hub) # show ⏳ in table
|
||||
|
||||
if proc.stdout is None:
|
||||
return
|
||||
for line in proc.stdout:
|
||||
logs.append(line.rstrip())
|
||||
|
||||
proc.wait()
|
||||
self._active_builds.pop(image, None)
|
||||
success = proc.returncode == 0
|
||||
self._build_results[image] = success
|
||||
self.call_from_thread(self._on_build_done, image, success=success)
|
||||
|
||||
def _on_build_done(self, image: str, *, success: bool) -> None:
|
||||
"""Handle completion of a background build on the main thread."""
|
||||
self._refresh_hub()
|
||||
if success:
|
||||
self.notify(f"✓ {image} built successfully", severity="information")
|
||||
else:
|
||||
self.notify(f"✗ {image} build failed — click row for log", severity="error")
|
||||
|
||||
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 secpipe_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 secpipe_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")
|
||||
687
secpipe-cli/src/secpipe_cli/tui/helpers.py
Normal file
687
secpipe-cli/src/secpipe_cli/tui/helpers.py
Normal file
@@ -0,0 +1,687 @@
|
||||
"""Shared helpers for SecPipe 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 contextlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from secpipe_cli.commands.mcp import (
|
||||
AIAgent,
|
||||
_detect_docker_socket,
|
||||
_detect_podman_socket,
|
||||
_find_secpipe_root,
|
||||
_generate_mcp_config,
|
||||
_get_claude_code_user_mcp_path,
|
||||
_get_claude_desktop_mcp_path,
|
||||
_get_copilot_mcp_path,
|
||||
)
|
||||
|
||||
# --- Hub Management Constants ---
|
||||
|
||||
SECPIPE_DEFAULT_HUB_URL = "git@github.com:FuzzingLabs/mcp-security-hub.git"
|
||||
SECPIPE_DEFAULT_HUB_NAME = "mcp-security-hub"
|
||||
|
||||
|
||||
def get_secpipe_user_dir() -> Path:
|
||||
"""Return the user-global ``~/.secpipe/`` directory.
|
||||
|
||||
Stores data that is shared across all workspaces: cloned hub
|
||||
repositories, the hub registry, container storage (graphroot/runroot),
|
||||
and the hub workspace volume.
|
||||
|
||||
Override with the ``SECPIPE_USER_DIR`` environment variable to
|
||||
redirect all user-global data to a custom path — useful for testing
|
||||
a fresh install without touching the real ``~/.secpipe/``.
|
||||
|
||||
:return: ``Path.home() / ".secpipe"`` or ``$SECPIPE_USER_DIR``
|
||||
|
||||
"""
|
||||
env_dir = os.environ.get("SECPIPE_USER_DIR")
|
||||
if env_dir:
|
||||
return Path(env_dir).resolve()
|
||||
return Path.home() / ".secpipe"
|
||||
|
||||
|
||||
def get_secpipe_dir() -> Path:
|
||||
"""Return the project-local ``.secpipe/`` directory.
|
||||
|
||||
Stores data that is specific to the current workspace: fuzzing
|
||||
results and project artifacts. Similar to how ``.git/`` scopes
|
||||
version-control data to a single project.
|
||||
|
||||
:return: ``Path.cwd() / ".secpipe"``
|
||||
|
||||
"""
|
||||
return Path.cwd() / ".secpipe"
|
||||
|
||||
# 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 SecPipe 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 "secpipe" 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 container image exists locally.
|
||||
|
||||
Respects the ``SECPIPE_ENGINE__TYPE`` environment variable so that
|
||||
Podman users see the correct build status instead of always "Not built".
|
||||
|
||||
:param image: Image name (e.g. "semgrep-mcp:latest").
|
||||
:return: Tuple of (is_ready, status_description).
|
||||
|
||||
"""
|
||||
engine = os.environ.get("SECPIPE_ENGINE__TYPE", "docker").lower()
|
||||
cmd = "podman" if engine == "podman" else "docker"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[cmd, "image", "inspect", image],
|
||||
check=False, 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, f"{cmd} not found"
|
||||
|
||||
|
||||
def load_hub_config(secpipe_root: Path) -> dict[str, Any]:
|
||||
"""Load hub-config.json from the SecPipe root.
|
||||
|
||||
:param secpipe_root: Path to secpipe-oss directory.
|
||||
:return: Parsed hub configuration dict, empty dict on error.
|
||||
|
||||
"""
|
||||
config_path = secpipe_root / "hub-config.json"
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
try:
|
||||
data: dict[str, Any] = json.loads(config_path.read_text())
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def find_secpipe_root() -> Path:
|
||||
"""Find the SecPipe installation root directory.
|
||||
|
||||
:return: Path to the secpipe-oss directory.
|
||||
|
||||
"""
|
||||
return _find_secpipe_root()
|
||||
|
||||
|
||||
def install_agent_config(agent: AIAgent, engine: str, force: bool = False) -> str:
|
||||
"""Install SecPipe 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.
|
||||
|
||||
"""
|
||||
secpipe_root = _find_secpipe_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(
|
||||
secpipe_root=secpipe_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 "secpipe" in servers and not force:
|
||||
return "Already configured (use force to overwrite)"
|
||||
|
||||
if servers_key not in existing:
|
||||
existing[servers_key] = {}
|
||||
existing[servers_key]["secpipe"] = server_config
|
||||
full_config = existing
|
||||
else:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
full_config = {servers_key: {"secpipe": server_config}}
|
||||
|
||||
config_path.write_text(json.dumps(full_config, indent=4))
|
||||
return f"Installed SecPipe for {agent.value}"
|
||||
|
||||
|
||||
def uninstall_agent_config(agent: AIAgent) -> str:
|
||||
"""Remove SecPipe 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 "secpipe" not in servers:
|
||||
return "SecPipe is not configured for this agent"
|
||||
|
||||
del servers["secpipe"]
|
||||
config_path.write_text(json.dumps(config, indent=4))
|
||||
return f"Removed SecPipe from {agent.value}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hub Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_hubs_registry_path() -> Path:
|
||||
"""Return path to the hubs registry file (``~/.secpipe/hubs.json``).
|
||||
|
||||
Stored in the user-global directory so the registry is shared across
|
||||
all workspaces.
|
||||
|
||||
:return: Path to the registry JSON file.
|
||||
|
||||
"""
|
||||
return get_secpipe_user_dir() / "hubs.json"
|
||||
|
||||
|
||||
def get_default_hubs_dir() -> Path:
|
||||
"""Return default directory for cloned hubs (``~/.secpipe/hubs/``).
|
||||
|
||||
Stored in the user-global directory so hubs are cloned once and
|
||||
reused in every workspace.
|
||||
|
||||
:return: Path to the default hubs directory.
|
||||
|
||||
"""
|
||||
return get_secpipe_user_dir() / "hubs"
|
||||
|
||||
|
||||
def _discover_hub_dirs() -> list[Path]:
|
||||
"""Scan known hub directories for cloned repos.
|
||||
|
||||
Checks both the current global location (``~/.secpipe/hubs/``) and the
|
||||
legacy workspace-local location (``<cwd>/.secpipe/hubs/``) so that hubs
|
||||
cloned before the global-dir migration are still found.
|
||||
|
||||
:return: List of hub directory paths (each is a direct child with a ``.git``
|
||||
sub-directory).
|
||||
|
||||
"""
|
||||
candidates: list[Path] = []
|
||||
for base in (get_secpipe_user_dir() / "hubs", get_secpipe_dir() / "hubs"):
|
||||
if base.is_dir():
|
||||
candidates.extend(
|
||||
entry for entry in base.iterdir()
|
||||
if entry.is_dir() and (entry / ".git").is_dir()
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
def load_hubs_registry() -> dict[str, Any]:
|
||||
"""Load the hubs registry from disk.
|
||||
|
||||
If the registry file does not exist, auto-recovers it by scanning known hub
|
||||
directories and rebuilding entries for any discovered hubs. This handles
|
||||
the migration from the old workspace-local ``<cwd>/.secpipe/hubs.json``
|
||||
path to the global ``~/.secpipe/hubs.json`` path, as well as any case
|
||||
where the registry was lost.
|
||||
|
||||
:return: Registry dict with ``hubs`` key containing a list of hub entries.
|
||||
|
||||
"""
|
||||
path = get_hubs_registry_path()
|
||||
if path.exists():
|
||||
try:
|
||||
data: dict[str, Any] = json.loads(path.read_text())
|
||||
return data
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
|
||||
# Registry missing — attempt to rebuild from discovered hub directories.
|
||||
discovered = _discover_hub_dirs()
|
||||
if not discovered:
|
||||
return {"hubs": []}
|
||||
|
||||
hubs: list[dict[str, Any]] = []
|
||||
for hub_dir in discovered:
|
||||
name = hub_dir.name
|
||||
# Try to read the git remote URL
|
||||
git_url: str = ""
|
||||
try:
|
||||
import subprocess as _sp
|
||||
r = _sp.run(
|
||||
["git", "-C", str(hub_dir), "remote", "get-url", "origin"],
|
||||
check=False, capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
git_url = r.stdout.strip()
|
||||
except Exception: # noqa: S110 - git URL is optional, failure is acceptable
|
||||
pass
|
||||
hubs.append({
|
||||
"name": name,
|
||||
"path": str(hub_dir),
|
||||
"git_url": git_url,
|
||||
"is_default": name == SECPIPE_DEFAULT_HUB_NAME,
|
||||
})
|
||||
|
||||
registry: dict[str, Any] = {"hubs": hubs}
|
||||
# Persist so we don't re-scan on every load
|
||||
with contextlib.suppress(OSError):
|
||||
save_hubs_registry(registry)
|
||||
return registry
|
||||
|
||||
|
||||
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_secpipe_user_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]
|
||||
name = name.removesuffix(".git")
|
||||
|
||||
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"],
|
||||
check=False, 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)],
|
||||
check=False, 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.
|
||||
|
||||
"""
|
||||
secpipe_root = find_secpipe_root()
|
||||
config_path = secpipe_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.
|
||||
|
||||
"""
|
||||
secpipe_root = find_secpipe_root()
|
||||
config_path = secpipe_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
|
||||
|
||||
|
||||
def find_dockerfile_for_server(server_name: str, hub_name: str) -> Path | None:
|
||||
"""Find the Dockerfile for a hub server tool.
|
||||
|
||||
Looks up the hub path from the registry, then scans for
|
||||
``category/<server_name>/Dockerfile``.
|
||||
|
||||
:param server_name: Tool name (e.g. ``"nmap-mcp"``).
|
||||
:param hub_name: Hub name as stored in the registry.
|
||||
:return: Absolute path to the Dockerfile, or ``None`` if not found.
|
||||
|
||||
"""
|
||||
registry = load_hubs_registry()
|
||||
hub_entry = next(
|
||||
(h for h in registry.get("hubs", []) if h.get("name") == hub_name),
|
||||
None,
|
||||
)
|
||||
if not hub_entry:
|
||||
return None
|
||||
|
||||
hub_path = Path(hub_entry["path"])
|
||||
for dockerfile in hub_path.rglob("Dockerfile"):
|
||||
rel = dockerfile.relative_to(hub_path)
|
||||
parts = rel.parts
|
||||
if len(parts) == 3 and parts[1] == server_name:
|
||||
return dockerfile
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def build_image(
|
||||
image: str,
|
||||
dockerfile: Path,
|
||||
*,
|
||||
engine: str | None = None,
|
||||
) -> subprocess.Popen[str]:
|
||||
"""Start a non-blocking ``docker/podman build`` subprocess.
|
||||
|
||||
Returns the running :class:`subprocess.Popen` object so the caller
|
||||
can stream ``stdout`` / ``stderr`` lines incrementally.
|
||||
|
||||
:param image: Image tag (e.g. ``"nmap-mcp:latest"``).
|
||||
:param dockerfile: Path to the ``Dockerfile``.
|
||||
:param engine: ``"docker"`` or ``"podman"`` (auto-detected if ``None``).
|
||||
:return: Running subprocess with merged stdout+stderr.
|
||||
|
||||
"""
|
||||
if engine is None:
|
||||
engine = os.environ.get("SECPIPE_ENGINE__TYPE", "docker").lower()
|
||||
engine = "podman" if engine == "podman" else "docker"
|
||||
|
||||
context_dir = str(dockerfile.parent)
|
||||
return subprocess.Popen(
|
||||
[engine, "build", "-t", image, context_dir],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
1
secpipe-cli/src/secpipe_cli/tui/screens/__init__.py
Normal file
1
secpipe-cli/src/secpipe_cli/tui/screens/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""TUI screens for SecPipe."""
|
||||
96
secpipe-cli/src/secpipe_cli/tui/screens/agent_setup.py
Normal file
96
secpipe-cli/src/secpipe_cli/tui/screens/agent_setup.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Agent setup and unlink modal screens for SecPipe 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 secpipe_cli.commands.mcp import AIAgent
|
||||
from secpipe_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 SecPipe 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)
|
||||
58
secpipe-cli/src/secpipe_cli/tui/screens/build_image.py
Normal file
58
secpipe-cli/src/secpipe_cli/tui/screens/build_image.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Build-image confirm dialog for SecPipe TUI.
|
||||
|
||||
Simple modal that asks the user to confirm before starting a background
|
||||
build. The actual build is managed by the app so the user is never
|
||||
locked on this screen.
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class _NoFocusButton(Button):
|
||||
can_focus = False
|
||||
|
||||
|
||||
class BuildImageScreen(ModalScreen[bool]):
|
||||
"""Quick confirmation before starting a background Docker/Podman build."""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def __init__(self, server_name: str, image: str, hub_name: str) -> None:
|
||||
super().__init__()
|
||||
self._server_name = server_name
|
||||
self._image = image
|
||||
self._hub_name = hub_name
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Build the confirmation dialog UI."""
|
||||
with Vertical(id="build-dialog"):
|
||||
yield Label(f"Build {self._image}", classes="dialog-title")
|
||||
yield Label(
|
||||
f"Hub: {self._hub_name} • Tool: {self._server_name}",
|
||||
id="build-subtitle",
|
||||
)
|
||||
yield Label(
|
||||
"The image will be built in the background.\n"
|
||||
"You'll receive a notification when it's done.",
|
||||
id="confirm-text",
|
||||
)
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield _NoFocusButton("Build", variant="primary", id="btn-build")
|
||||
yield _NoFocusButton("Cancel", variant="default", id="btn-cancel")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle Build or Cancel button clicks."""
|
||||
if event.button.id == "btn-build":
|
||||
self.dismiss(result=True)
|
||||
elif event.button.id == "btn-cancel":
|
||||
self.dismiss(result=False)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Dismiss the dialog when Escape is pressed."""
|
||||
self.dismiss(result=False)
|
||||
80
secpipe-cli/src/secpipe_cli/tui/screens/build_log.py
Normal file
80
secpipe-cli/src/secpipe_cli/tui/screens/build_log.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Build-log viewer screen for SecPipe TUI.
|
||||
|
||||
Shows live output of a background build started by the app. Polls the
|
||||
app's ``_build_logs`` buffer every 500 ms so the user can pop this screen
|
||||
open at any time while the build is running and see up-to-date output.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Label, Log
|
||||
|
||||
|
||||
class _NoFocusButton(Button):
|
||||
can_focus = False
|
||||
|
||||
|
||||
class BuildLogScreen(ModalScreen[None]):
|
||||
"""Live log viewer for a background build job managed by the app."""
|
||||
|
||||
BINDINGS = [("escape", "close", "Close")]
|
||||
|
||||
def __init__(self, image: str) -> None:
|
||||
super().__init__()
|
||||
self._image = image
|
||||
self._last_line: int = 0
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Build the log viewer UI."""
|
||||
with Vertical(id="build-dialog"):
|
||||
yield Label(f"Build log — {self._image}", classes="dialog-title")
|
||||
yield Label("", id="build-status")
|
||||
yield Log(id="build-log", auto_scroll=True)
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield _NoFocusButton("Close", variant="default", id="btn-close")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize log polling when the screen is mounted."""
|
||||
self._flush_log()
|
||||
self.set_interval(0.5, self._poll_log)
|
||||
|
||||
def _flush_log(self) -> None:
|
||||
"""Write any new lines since the last flush."""
|
||||
logs: list[str] = getattr(self.app, "_build_logs", {}).get(self._image, [])
|
||||
log_widget = self.query_one("#build-log", Log)
|
||||
new_lines = logs[self._last_line :]
|
||||
for line in new_lines:
|
||||
log_widget.write_line(line)
|
||||
self._last_line += len(new_lines)
|
||||
|
||||
active: dict[str, Any] = getattr(self.app, "_active_builds", {})
|
||||
status = self.query_one("#build-status", Label)
|
||||
if self._image in active:
|
||||
status.update("[yellow]⏳ Building…[/yellow]")
|
||||
else:
|
||||
# Build is done — check if we have a result stored
|
||||
results: dict[str, Any] = getattr(self.app, "_build_results", {})
|
||||
if self._image in results:
|
||||
if results[self._image]:
|
||||
status.update(f"[green]✓ {self._image} built successfully[/green]")
|
||||
else:
|
||||
status.update(f"[red]✗ {self._image} build failed[/red]")
|
||||
|
||||
def _poll_log(self) -> None:
|
||||
"""Poll for new log lines periodically."""
|
||||
self._flush_log()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle Close button click."""
|
||||
if event.button.id == "btn-close":
|
||||
self.dismiss(None)
|
||||
|
||||
def action_close(self) -> None:
|
||||
"""Dismiss the dialog when Escape is pressed."""
|
||||
self.dismiss(None)
|
||||
301
secpipe-cli/src/secpipe_cli/tui/screens/hub_manager.py
Normal file
301
secpipe-cli/src/secpipe_cli/tui/screens/hub_manager.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""Hub management screens for SecPipe 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 secpipe_cli.tui.helpers import (
|
||||
SECPIPE_DEFAULT_HUB_NAME,
|
||||
SECPIPE_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)
|
||||
count: str | Text
|
||||
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"
|
||||
name_cell: str | Text
|
||||
if is_default:
|
||||
name_cell = Text(f"★ {name}", style="bold")
|
||||
else:
|
||||
name_cell = 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(
|
||||
SECPIPE_DEFAULT_HUB_URL,
|
||||
SECPIPE_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)
|
||||
3
secpipe-common/README.md
Normal file
3
secpipe-common/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# SecPipe Common
|
||||
|
||||
...
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "fuzzforge-common"
|
||||
name = "secpipe-common"
|
||||
version = "0.0.1"
|
||||
description = "FuzzForge's common types and utilities."
|
||||
description = "SecPipe's common types and utilities."
|
||||
authors = []
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.14"
|
||||
49
secpipe-common/ruff.toml
Normal file
49
secpipe-common/ruff.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [ "ALL" ]
|
||||
ignore = [
|
||||
"COM812", # conflicts with the formatter
|
||||
"D100", # ignoring missing docstrings in public modules
|
||||
"D104", # ignoring missing docstrings in public packages
|
||||
"D203", # conflicts with 'D211'
|
||||
"D213", # conflicts with 'D212'
|
||||
"TD002", # ignoring missing author in 'TODO' statements
|
||||
"TD003", # ignoring missing issue link in 'TODO' statements
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"ANN401", # allowing 'typing.Any' to be used to type function parameters in tests
|
||||
"PLR2004", # allowing comparisons using unamed numerical constants in tests
|
||||
"S101", # allowing 'assert' statements in tests
|
||||
]
|
||||
"src/**" = [
|
||||
"ANN201", # missing return type: legacy code
|
||||
"ARG002", # unused argument: callback pattern
|
||||
"ASYNC109", # async with timeout param: intentional pattern
|
||||
"BLE001", # blind exception: broad error handling needed
|
||||
"C901", # complexity: legacy code
|
||||
"EM102", # f-string in exception: existing pattern
|
||||
"F401", # unused import: re-export pattern
|
||||
"FBT001", # boolean positional arg
|
||||
"FBT002", # boolean default arg
|
||||
"FIX002", # TODO comments: documented tech debt
|
||||
"N806", # variable naming: intentional constants
|
||||
"PERF401", # list comprehension: readability over perf
|
||||
"PLW0603", # global statement: intentional for shared state
|
||||
"PTH111", # os.path usage: legacy code
|
||||
"RUF005", # collection literal: legacy style
|
||||
"S110", # try-except-pass: intentional suppression
|
||||
"S603", # subprocess: validated inputs
|
||||
"SIM108", # ternary: readability preference
|
||||
"TC001", # TYPE_CHECKING: causes circular imports
|
||||
"TC003", # TYPE_CHECKING: causes circular imports
|
||||
"TRY003", # message in exception: existing pattern
|
||||
"TRY300", # try-else: existing pattern
|
||||
"TRY400", # logging.error vs exception: existing pattern
|
||||
"UP017", # datetime.UTC: Python 3.11+ only
|
||||
"UP041", # TimeoutError alias: compatibility
|
||||
"UP043", # unnecessary type args: compatibility
|
||||
"W293", # blank line whitespace: formatting
|
||||
]
|
||||
38
secpipe-common/src/secpipe_common/__init__.py
Normal file
38
secpipe-common/src/secpipe_common/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""SecPipe Common - Shared abstractions and implementations for SecPipe.
|
||||
|
||||
This package provides:
|
||||
- Sandbox engine abstractions (Podman, Docker)
|
||||
- Common exceptions
|
||||
|
||||
Example usage:
|
||||
from secpipe_common import (
|
||||
AbstractSecPipeSandboxEngine,
|
||||
ImageInfo,
|
||||
Podman,
|
||||
PodmanConfiguration,
|
||||
)
|
||||
"""
|
||||
|
||||
from secpipe_common.exceptions import SecPipeError
|
||||
from secpipe_common.sandboxes import (
|
||||
AbstractSecPipeEngineConfiguration,
|
||||
AbstractSecPipeSandboxEngine,
|
||||
Docker,
|
||||
DockerConfiguration,
|
||||
SecPipeSandboxEngines,
|
||||
ImageInfo,
|
||||
Podman,
|
||||
PodmanConfiguration,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbstractSecPipeEngineConfiguration",
|
||||
"AbstractSecPipeSandboxEngine",
|
||||
"Docker",
|
||||
"DockerConfiguration",
|
||||
"SecPipeError",
|
||||
"SecPipeSandboxEngines",
|
||||
"ImageInfo",
|
||||
"Podman",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -4,8 +4,8 @@ if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class FuzzForgeError(Exception):
|
||||
"""Base exception for all FuzzForge custom exceptions.
|
||||
class SecPipeError(Exception):
|
||||
"""Base exception for all SecPipe custom exceptions.
|
||||
|
||||
All domain exceptions should inherit from this base to enable
|
||||
consistent exception handling and hierarchy navigation.
|
||||
@@ -13,7 +13,7 @@ class FuzzForgeError(Exception):
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
|
||||
"""Initialize FuzzForge error.
|
||||
"""Initialize SecPipe error.
|
||||
|
||||
:param message: Error message.
|
||||
:param details: Optional error details dictionary.
|
||||
@@ -1,8 +1,8 @@
|
||||
"""FuzzForge Hub - Generic MCP server bridge.
|
||||
"""SecPipe Hub - Generic MCP server bridge.
|
||||
|
||||
This module provides a generic bridge to connect FuzzForge with any MCP server.
|
||||
This module provides a generic bridge to connect SecPipe with any MCP server.
|
||||
It allows AI agents to discover and execute tools from external MCP servers
|
||||
(like mcp-security-hub) through the same interface as native FuzzForge modules.
|
||||
(like mcp-security-hub) through the same interface as native SecPipe modules.
|
||||
|
||||
The hub is server-agnostic: it doesn't hardcode any specific tools or servers.
|
||||
Instead, it dynamically discovers tools by connecting to configured MCP servers
|
||||
@@ -15,9 +15,9 @@ Supported transport types:
|
||||
|
||||
"""
|
||||
|
||||
from fuzzforge_common.hub.client import HubClient, HubClientError, PersistentSession
|
||||
from fuzzforge_common.hub.executor import HubExecutionResult, HubExecutor
|
||||
from fuzzforge_common.hub.models import (
|
||||
from secpipe_common.hub.client import HubClient, HubClientError, PersistentSession
|
||||
from secpipe_common.hub.executor import HubExecutionResult, HubExecutor
|
||||
from secpipe_common.hub.models import (
|
||||
HubConfig,
|
||||
HubServer,
|
||||
HubServerConfig,
|
||||
@@ -25,7 +25,7 @@ from fuzzforge_common.hub.models import (
|
||||
HubTool,
|
||||
HubToolParameter,
|
||||
)
|
||||
from fuzzforge_common.hub.registry import HubRegistry
|
||||
from secpipe_common.hub.registry import HubRegistry
|
||||
|
||||
__all__ = [
|
||||
"HubClient",
|
||||
@@ -21,7 +21,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from fuzzforge_common.hub.models import (
|
||||
from secpipe_common.hub.models import (
|
||||
HubServer,
|
||||
HubServerConfig,
|
||||
HubServerType,
|
||||
@@ -176,6 +176,7 @@ class HubClient:
|
||||
arguments: dict[str, Any],
|
||||
*,
|
||||
timeout: int | None = None,
|
||||
extra_volumes: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a tool on a hub server.
|
||||
|
||||
@@ -183,6 +184,7 @@ class HubClient:
|
||||
:param tool_name: Name of the tool to execute.
|
||||
:param arguments: Tool arguments.
|
||||
:param timeout: Execution timeout (uses default if None).
|
||||
:param extra_volumes: Additional Docker volume mounts to inject.
|
||||
:returns: Tool execution result.
|
||||
:raises HubClientError: If execution fails.
|
||||
|
||||
@@ -199,7 +201,7 @@ class HubClient:
|
||||
)
|
||||
|
||||
try:
|
||||
async with self._connect(config) as (reader, writer):
|
||||
async with self._connect(config, extra_volumes=extra_volumes) as (reader, writer):
|
||||
# Initialise MCP session (skip for persistent — already done)
|
||||
if not self._persistent_sessions.get(config.name):
|
||||
await self._initialize_session(reader, writer, config.name)
|
||||
@@ -248,6 +250,7 @@ class HubClient:
|
||||
async def _connect(
|
||||
self,
|
||||
config: HubServerConfig,
|
||||
extra_volumes: list[str] | None = None,
|
||||
) -> AsyncGenerator[tuple[asyncio.StreamReader, asyncio.StreamWriter], None]:
|
||||
"""Connect to an MCP server.
|
||||
|
||||
@@ -256,6 +259,7 @@ class HubClient:
|
||||
ephemeral per-call connection logic.
|
||||
|
||||
:param config: Server configuration.
|
||||
:param extra_volumes: Additional Docker volume mounts to inject.
|
||||
:yields: Tuple of (reader, writer) for communication.
|
||||
|
||||
"""
|
||||
@@ -268,7 +272,7 @@ class HubClient:
|
||||
|
||||
# Ephemeral connection (original behaviour)
|
||||
if config.type == HubServerType.DOCKER:
|
||||
async with self._connect_docker(config) as streams:
|
||||
async with self._connect_docker(config, extra_volumes=extra_volumes) as streams:
|
||||
yield streams
|
||||
elif config.type == HubServerType.COMMAND:
|
||||
async with self._connect_command(config) as streams:
|
||||
@@ -284,10 +288,12 @@ class HubClient:
|
||||
async def _connect_docker(
|
||||
self,
|
||||
config: HubServerConfig,
|
||||
extra_volumes: list[str] | None = None,
|
||||
) -> AsyncGenerator[tuple[asyncio.StreamReader, asyncio.StreamWriter], None]:
|
||||
"""Connect to a Docker-based MCP server.
|
||||
|
||||
:param config: Server configuration with image name.
|
||||
:param extra_volumes: Additional volume mounts to inject (e.g. project assets).
|
||||
:yields: Tuple of (reader, writer) for stdio communication.
|
||||
|
||||
"""
|
||||
@@ -302,10 +308,14 @@ class HubClient:
|
||||
for cap in config.capabilities:
|
||||
cmd.extend(["--cap-add", cap])
|
||||
|
||||
# Add volumes
|
||||
# Add volumes from server config
|
||||
for volume in config.volumes:
|
||||
cmd.extend(["-v", os.path.expanduser(volume)])
|
||||
|
||||
# Add extra volumes (e.g. project assets injected at runtime)
|
||||
for volume in (extra_volumes or []):
|
||||
cmd.extend(["-v", os.path.expanduser(volume)])
|
||||
|
||||
# Add environment variables
|
||||
for key, value in config.environment.items():
|
||||
cmd.extend(["-e", f"{key}={value}"])
|
||||
@@ -427,7 +437,7 @@ class HubClient:
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"clientInfo": {
|
||||
"name": "fuzzforge-hub",
|
||||
"name": "secpipe-hub",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
},
|
||||
@@ -529,6 +539,7 @@ class HubClient:
|
||||
async def start_persistent_session(
|
||||
self,
|
||||
config: HubServerConfig,
|
||||
extra_volumes: list[str] | None = None,
|
||||
) -> PersistentSession:
|
||||
"""Start a persistent Docker container and initialise MCP session.
|
||||
|
||||
@@ -536,6 +547,7 @@ class HubClient:
|
||||
called, allowing multiple tool calls on the same session.
|
||||
|
||||
:param config: Server configuration (must be Docker type).
|
||||
:param extra_volumes: Additional host:container volume mounts to inject.
|
||||
:returns: The created persistent session.
|
||||
:raises HubClientError: If the container cannot be started.
|
||||
|
||||
@@ -558,7 +570,7 @@ class HubClient:
|
||||
msg = f"Docker image not specified for server '{config.name}'"
|
||||
raise HubClientError(msg)
|
||||
|
||||
container_name = f"fuzzforge-{config.name}"
|
||||
container_name = f"secpipe-{config.name}"
|
||||
|
||||
# Remove stale container with same name if it exists
|
||||
try:
|
||||
@@ -580,6 +592,9 @@ class HubClient:
|
||||
for volume in config.volumes:
|
||||
cmd.extend(["-v", os.path.expanduser(volume)])
|
||||
|
||||
for extra_vol in (extra_volumes or []):
|
||||
cmd.extend(["-v", extra_vol])
|
||||
|
||||
for key, value in config.environment.items():
|
||||
cmd.extend(["-e", f"{key}={value}"])
|
||||
|
||||
@@ -12,9 +12,9 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from fuzzforge_common.hub.client import HubClient, HubClientError, PersistentSession
|
||||
from fuzzforge_common.hub.models import HubServer, HubServerConfig, HubTool
|
||||
from fuzzforge_common.hub.registry import HubRegistry
|
||||
from secpipe_common.hub.client import HubClient, HubClientError, PersistentSession
|
||||
from secpipe_common.hub.models import HubServer, HubServerConfig, HubTool
|
||||
from secpipe_common.hub.registry import HubRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from structlog.stdlib import BoundLogger
|
||||
@@ -180,12 +180,14 @@ class HubExecutor:
|
||||
arguments: dict[str, Any] | None = None,
|
||||
*,
|
||||
timeout: int | None = None,
|
||||
extra_volumes: list[str] | None = None,
|
||||
) -> HubExecutionResult:
|
||||
"""Execute a hub tool.
|
||||
|
||||
:param identifier: Tool identifier (hub:server:tool or server:tool).
|
||||
:param arguments: Tool arguments.
|
||||
:param timeout: Execution timeout.
|
||||
:param extra_volumes: Additional Docker volume mounts to inject.
|
||||
:returns: Execution result.
|
||||
|
||||
"""
|
||||
@@ -232,6 +234,7 @@ class HubExecutor:
|
||||
tool_name_to_use or tool_name,
|
||||
arguments,
|
||||
timeout=timeout,
|
||||
extra_volumes=extra_volumes,
|
||||
)
|
||||
return HubExecutionResult(
|
||||
success=True,
|
||||
@@ -268,6 +271,7 @@ class HubExecutor:
|
||||
tool.name,
|
||||
arguments,
|
||||
timeout=timeout,
|
||||
extra_volumes=extra_volumes,
|
||||
)
|
||||
return HubExecutionResult(
|
||||
success=True,
|
||||
@@ -341,13 +345,14 @@ class HubExecutor:
|
||||
# Persistent session management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def start_persistent_server(self, server_name: str) -> dict[str, Any]:
|
||||
async def start_persistent_server(self, server_name: str, extra_volumes: list[str] | None = None) -> dict[str, Any]:
|
||||
"""Start a persistent container session for a server.
|
||||
|
||||
The container stays running between tool calls, allowing stateful
|
||||
interactions (e.g., radare2 sessions, long-running fuzzing).
|
||||
|
||||
:param server_name: Name of the hub server to start.
|
||||
:param extra_volumes: Additional host:container volume mounts to inject.
|
||||
:returns: Session status dictionary.
|
||||
:raises ValueError: If server not found.
|
||||
|
||||
@@ -358,7 +363,7 @@ class HubExecutor:
|
||||
msg = f"Server '{server_name}' not found"
|
||||
raise ValueError(msg)
|
||||
|
||||
session = await self._client.start_persistent_session(server.config)
|
||||
session = await self._client.start_persistent_session(server.config, extra_volumes=extra_volumes)
|
||||
|
||||
# Auto-discover tools on the new session
|
||||
try:
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Data models for FuzzForge Hub.
|
||||
"""Data models for SecPipe Hub.
|
||||
|
||||
This module defines the Pydantic models used to represent MCP servers
|
||||
and their tools in the hub registry.
|
||||
@@ -294,3 +294,17 @@ class HubConfig(BaseModel):
|
||||
default=True,
|
||||
description="Cache discovered tools",
|
||||
)
|
||||
|
||||
#: Workflow hints indexed by "after:<tool_name>" keys.
|
||||
#: Loaded inline or merged from workflow_hints_file.
|
||||
workflow_hints: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Workflow hints indexed by 'after:<tool_name>'",
|
||||
)
|
||||
|
||||
#: Optional path to an external workflow-hints.json file.
|
||||
#: Relative paths are resolved relative to the hub-config.json location.
|
||||
workflow_hints_file: str | None = Field(
|
||||
default=None,
|
||||
description="Path to an external workflow-hints.json to load and merge",
|
||||
)
|
||||
@@ -12,7 +12,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from fuzzforge_common.hub.models import (
|
||||
from secpipe_common.hub.models import (
|
||||
HubConfig,
|
||||
HubServer,
|
||||
HubServerConfig,
|
||||
@@ -87,6 +87,28 @@ class HubRegistry:
|
||||
config=server_config,
|
||||
)
|
||||
|
||||
# Load and merge external workflow hints file if specified.
|
||||
if self._config.workflow_hints_file:
|
||||
hints_path = Path(self._config.workflow_hints_file)
|
||||
if not hints_path.is_absolute():
|
||||
hints_path = config_path.parent / hints_path
|
||||
if hints_path.exists():
|
||||
try:
|
||||
with hints_path.open() as hf:
|
||||
hints_data = json.load(hf)
|
||||
self._config.workflow_hints.update(hints_data.get("hints", {}))
|
||||
logger.info(
|
||||
"Loaded workflow hints",
|
||||
path=str(hints_path),
|
||||
hints=len(self._config.workflow_hints),
|
||||
)
|
||||
except Exception as hints_err:
|
||||
logger.warning(
|
||||
"Failed to load workflow hints file",
|
||||
path=str(hints_path),
|
||||
error=str(hints_err),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Loaded hub configuration",
|
||||
path=str(config_path),
|
||||
@@ -218,6 +240,15 @@ class HubRegistry:
|
||||
server.discovery_error = None
|
||||
server.tools = tools
|
||||
|
||||
def get_workflow_hint(self, tool_name: str) -> dict | None:
|
||||
"""Get the workflow hint for a tool by name.
|
||||
|
||||
:param tool_name: Tool name (e.g. ``binwalk_extract``).
|
||||
:returns: Hint dict for the ``after:<tool_name>`` key, or None.
|
||||
|
||||
"""
|
||||
return self._config.workflow_hints.get(f"after:{tool_name}") or None
|
||||
|
||||
def get_all_tools(self) -> list:
|
||||
"""Get all discovered tools from all servers.
|
||||
|
||||
23
secpipe-common/src/secpipe_common/sandboxes/__init__.py
Normal file
23
secpipe-common/src/secpipe_common/sandboxes/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""SecPipe sandbox abstractions and implementations."""
|
||||
|
||||
from secpipe_common.sandboxes.engines import (
|
||||
AbstractSecPipeEngineConfiguration,
|
||||
AbstractSecPipeSandboxEngine,
|
||||
Docker,
|
||||
DockerConfiguration,
|
||||
SecPipeSandboxEngines,
|
||||
ImageInfo,
|
||||
Podman,
|
||||
PodmanConfiguration,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbstractSecPipeEngineConfiguration",
|
||||
"AbstractSecPipeSandboxEngine",
|
||||
"Docker",
|
||||
"DockerConfiguration",
|
||||
"SecPipeSandboxEngines",
|
||||
"ImageInfo",
|
||||
"Podman",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Container engine implementations for SecPipe sandboxes."""
|
||||
|
||||
from secpipe_common.sandboxes.engines.base import (
|
||||
AbstractSecPipeEngineConfiguration,
|
||||
AbstractSecPipeSandboxEngine,
|
||||
ImageInfo,
|
||||
)
|
||||
from secpipe_common.sandboxes.engines.docker import Docker, DockerConfiguration
|
||||
from secpipe_common.sandboxes.engines.enumeration import SecPipeSandboxEngines
|
||||
from secpipe_common.sandboxes.engines.podman import Podman, PodmanConfiguration
|
||||
|
||||
__all__ = [
|
||||
"AbstractSecPipeEngineConfiguration",
|
||||
"AbstractSecPipeSandboxEngine",
|
||||
"Docker",
|
||||
"DockerConfiguration",
|
||||
"SecPipeSandboxEngines",
|
||||
"ImageInfo",
|
||||
"Podman",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Base engine abstractions."""
|
||||
|
||||
from secpipe_common.sandboxes.engines.base.configuration import (
|
||||
AbstractSecPipeEngineConfiguration,
|
||||
)
|
||||
from secpipe_common.sandboxes.engines.base.engine import (
|
||||
AbstractSecPipeSandboxEngine,
|
||||
ImageInfo,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AbstractSecPipeEngineConfiguration",
|
||||
"AbstractSecPipeSandboxEngine",
|
||||
"ImageInfo",
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from secpipe_common.sandboxes.engines.enumeration import (
|
||||
SecPipeSandboxEngines,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine
|
||||
|
||||
|
||||
class AbstractSecPipeEngineConfiguration(ABC, BaseModel):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
kind: SecPipeSandboxEngines
|
||||
|
||||
@abstractmethod
|
||||
def into_engine(self) -> AbstractSecPipeSandboxEngine:
|
||||
"""TODO."""
|
||||
message: str = f"method 'into_engine' is not implemented for class '{self.__class__.__name__}'"
|
||||
raise NotImplementedError(message)
|
||||
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
||||
class ImageInfo:
|
||||
"""Information about a container image."""
|
||||
|
||||
#: Full image reference (e.g., "localhost/fuzzforge-module-echidna:latest").
|
||||
#: Full image reference (e.g., "localhost/secpipe-module-echidna:latest").
|
||||
reference: str
|
||||
|
||||
#: Repository name (e.g., "localhost/fuzzforge-module-echidna").
|
||||
#: Repository name (e.g., "localhost/secpipe-module-echidna").
|
||||
repository: str
|
||||
|
||||
#: Image tag (e.g., "latest").
|
||||
@@ -29,8 +29,8 @@ class ImageInfo:
|
||||
labels: dict[str, str] | None = None
|
||||
|
||||
|
||||
class AbstractFuzzForgeSandboxEngine(ABC):
|
||||
"""Abstract class used as a base for all FuzzForge sandbox engine classes."""
|
||||
class AbstractSecPipeSandboxEngine(ABC):
|
||||
"""Abstract class used as a base for all SecPipe sandbox engine classes."""
|
||||
|
||||
@abstractmethod
|
||||
def list_images(self, filter_prefix: str | None = None) -> list[ImageInfo]:
|
||||
@@ -140,7 +140,7 @@ class AbstractFuzzForgeSandboxEngine(ABC):
|
||||
|
||||
:param image: Full image reference to pull.
|
||||
:param timeout: Timeout in seconds for the pull operation.
|
||||
:raises FuzzForgeError: If pull fails.
|
||||
:raises SecPipeError: If pull fails.
|
||||
|
||||
"""
|
||||
message: str = f"method 'pull_image' is not implemented for class '{self.__class__.__name__}'"
|
||||
@@ -306,7 +306,7 @@ class AbstractFuzzForgeSandboxEngine(ABC):
|
||||
|
||||
Creates a temporary container, copies the file, and removes the container.
|
||||
|
||||
:param image: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
|
||||
:param image: Image reference (e.g., "secpipe-rust-analyzer:latest").
|
||||
:param path: Path to file inside image.
|
||||
:returns: File contents as string.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Docker container engine implementation."""
|
||||
|
||||
from secpipe_common.sandboxes.engines.docker.cli import DockerCLI
|
||||
from secpipe_common.sandboxes.engines.docker.configuration import (
|
||||
DockerConfiguration,
|
||||
)
|
||||
from secpipe_common.sandboxes.engines.docker.engine import Docker
|
||||
|
||||
__all__ = [
|
||||
"Docker",
|
||||
"DockerCLI",
|
||||
"DockerConfiguration",
|
||||
]
|
||||
@@ -13,8 +13,8 @@ from pathlib import Path, PurePath
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from fuzzforge_common.exceptions import FuzzForgeError
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine, ImageInfo
|
||||
from secpipe_common.exceptions import SecPipeError
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine, ImageInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from structlog.stdlib import BoundLogger
|
||||
@@ -27,7 +27,7 @@ def get_logger() -> BoundLogger:
|
||||
return cast("BoundLogger", get_logger())
|
||||
|
||||
|
||||
class DockerCLI(AbstractFuzzForgeSandboxEngine):
|
||||
class DockerCLI(AbstractSecPipeSandboxEngine):
|
||||
"""Docker engine using CLI commands.
|
||||
|
||||
This implementation uses subprocess calls to the Docker CLI,
|
||||
@@ -37,7 +37,7 @@ class DockerCLI(AbstractFuzzForgeSandboxEngine):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the DockerCLI engine."""
|
||||
AbstractFuzzForgeSandboxEngine.__init__(self)
|
||||
AbstractSecPipeSandboxEngine.__init__(self)
|
||||
|
||||
def _base_cmd(self) -> list[str]:
|
||||
"""Get base Docker command.
|
||||
@@ -147,7 +147,7 @@ class DockerCLI(AbstractFuzzForgeSandboxEngine):
|
||||
get_logger().info("image pulled successfully", image=image)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
message = f"Failed to pull image '{image}': {exc.stderr}"
|
||||
raise FuzzForgeError(message) from exc
|
||||
raise SecPipeError(message) from exc
|
||||
|
||||
def tag_image(self, source: str, target: str) -> None:
|
||||
"""Tag an image with a new name.
|
||||
@@ -440,7 +440,7 @@ class DockerCLI(AbstractFuzzForgeSandboxEngine):
|
||||
|
||||
Uses docker run with --entrypoint override to read the file via cat.
|
||||
|
||||
:param image: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
|
||||
:param image: Image reference (e.g., "secpipe-rust-analyzer:latest").
|
||||
:param path: Path to file inside image.
|
||||
:returns: File contents as string.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from secpipe_common.sandboxes.engines.base.configuration import AbstractSecPipeEngineConfiguration
|
||||
from secpipe_common.sandboxes.engines.docker.engine import Docker
|
||||
from secpipe_common.sandboxes.engines.enumeration import SecPipeSandboxEngines
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine
|
||||
|
||||
|
||||
class DockerConfiguration(AbstractSecPipeEngineConfiguration):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
kind: Literal[SecPipeSandboxEngines.DOCKER] = SecPipeSandboxEngines.DOCKER
|
||||
|
||||
#: TODO.
|
||||
socket: str
|
||||
|
||||
def into_engine(self) -> AbstractSecPipeSandboxEngine:
|
||||
"""TODO."""
|
||||
return Docker(socket=self.socket)
|
||||
@@ -2,13 +2,13 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine, ImageInfo
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine, ImageInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path, PurePath
|
||||
|
||||
|
||||
class Docker(AbstractFuzzForgeSandboxEngine):
|
||||
class Docker(AbstractSecPipeSandboxEngine):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class FuzzForgeSandboxEngines(StrEnum):
|
||||
class SecPipeSandboxEngines(StrEnum):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Podman container engine implementation."""
|
||||
|
||||
from secpipe_common.sandboxes.engines.podman.cli import PodmanCLI
|
||||
from secpipe_common.sandboxes.engines.podman.configuration import (
|
||||
PodmanConfiguration,
|
||||
)
|
||||
from secpipe_common.sandboxes.engines.podman.engine import Podman
|
||||
|
||||
__all__ = [
|
||||
"Podman",
|
||||
"PodmanCLI",
|
||||
"PodmanConfiguration",
|
||||
]
|
||||
@@ -15,8 +15,8 @@ from pathlib import Path, PurePath
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from fuzzforge_common.exceptions import FuzzForgeError
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine, ImageInfo
|
||||
from secpipe_common.exceptions import SecPipeError
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine, ImageInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from structlog.stdlib import BoundLogger
|
||||
@@ -43,7 +43,7 @@ def _is_running_under_snap() -> bool:
|
||||
return os.getenv("SNAP") is not None
|
||||
|
||||
|
||||
class PodmanCLI(AbstractFuzzForgeSandboxEngine):
|
||||
class PodmanCLI(AbstractSecPipeSandboxEngine):
|
||||
"""Podman engine using CLI with custom storage paths.
|
||||
|
||||
This implementation uses subprocess calls to the Podman CLI with --root
|
||||
@@ -71,7 +71,7 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine):
|
||||
|
||||
Custom storage is used when running under Snap AND paths are provided.
|
||||
|
||||
:raises FuzzForgeError: If running on macOS (Podman not supported).
|
||||
:raises SecPipeError: If running on macOS (Podman not supported).
|
||||
"""
|
||||
import sys # noqa: PLC0415
|
||||
|
||||
@@ -81,9 +81,9 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine):
|
||||
" brew install --cask docker\n"
|
||||
" # Or download from https://docker.com/products/docker-desktop"
|
||||
)
|
||||
raise FuzzForgeError(msg)
|
||||
raise SecPipeError(msg)
|
||||
|
||||
AbstractFuzzForgeSandboxEngine.__init__(self)
|
||||
AbstractSecPipeSandboxEngine.__init__(self)
|
||||
|
||||
# Use custom storage only under Snap (to fix XDG_DATA_HOME issues)
|
||||
self.__use_custom_storage = _is_running_under_snap() and graphroot is not None and runroot is not None
|
||||
@@ -206,7 +206,7 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine):
|
||||
get_logger().info("image pulled successfully", image=image)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
message = f"Failed to pull image '{image}': {exc.stderr}"
|
||||
raise FuzzForgeError(message) from exc
|
||||
raise SecPipeError(message) from exc
|
||||
|
||||
def tag_image(self, source: str, target: str) -> None:
|
||||
"""Tag an image with a new name.
|
||||
@@ -501,7 +501,7 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine):
|
||||
|
||||
Uses podman run with --entrypoint override to read the file via cat.
|
||||
|
||||
:param image: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
|
||||
:param image: Image reference (e.g., "secpipe-rust-analyzer:latest").
|
||||
:param path: Path to file inside image.
|
||||
:returns: File contents as string.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from secpipe_common.sandboxes.engines.base.configuration import AbstractSecPipeEngineConfiguration
|
||||
from secpipe_common.sandboxes.engines.enumeration import SecPipeSandboxEngines
|
||||
from secpipe_common.sandboxes.engines.podman.engine import Podman
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine
|
||||
|
||||
|
||||
class PodmanConfiguration(AbstractSecPipeEngineConfiguration):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
kind: Literal[SecPipeSandboxEngines.PODMAN] = SecPipeSandboxEngines.PODMAN
|
||||
|
||||
#: TODO.
|
||||
socket: str
|
||||
|
||||
def into_engine(self) -> AbstractSecPipeSandboxEngine:
|
||||
"""TODO."""
|
||||
return Podman(socket=self.socket)
|
||||
@@ -8,8 +8,8 @@ from typing import TYPE_CHECKING, cast
|
||||
|
||||
from podman.errors import ImageNotFound
|
||||
|
||||
from fuzzforge_common.exceptions import FuzzForgeError
|
||||
from fuzzforge_common.sandboxes.engines.base.engine import AbstractFuzzForgeSandboxEngine, ImageInfo
|
||||
from secpipe_common.exceptions import SecPipeError
|
||||
from secpipe_common.sandboxes.engines.base.engine import AbstractSecPipeSandboxEngine, ImageInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from podman import PodmanClient
|
||||
@@ -24,7 +24,7 @@ def get_logger() -> BoundLogger:
|
||||
return cast("BoundLogger", get_logger())
|
||||
|
||||
|
||||
class Podman(AbstractFuzzForgeSandboxEngine):
|
||||
class Podman(AbstractSecPipeSandboxEngine):
|
||||
"""TODO."""
|
||||
|
||||
#: TODO.
|
||||
@@ -36,7 +36,7 @@ class Podman(AbstractFuzzForgeSandboxEngine):
|
||||
:param socket: TODO.
|
||||
|
||||
"""
|
||||
AbstractFuzzForgeSandboxEngine.__init__(self)
|
||||
AbstractSecPipeSandboxEngine.__init__(self)
|
||||
self.__socket = socket
|
||||
|
||||
def get_client(self) -> PodmanClient:
|
||||
@@ -99,7 +99,7 @@ class Podman(AbstractFuzzForgeSandboxEngine):
|
||||
images = list(client.images.load(file_path=archive))
|
||||
if len(images) != 1:
|
||||
message: str = "expected only one image"
|
||||
raise FuzzForgeError(message)
|
||||
raise SecPipeError(message)
|
||||
image = images[0]
|
||||
image.tag(repository=repository, tag="latest")
|
||||
|
||||
@@ -254,7 +254,7 @@ class Podman(AbstractFuzzForgeSandboxEngine):
|
||||
|
||||
:param image: Full image reference to pull.
|
||||
:param timeout: Timeout in seconds for the pull operation.
|
||||
:raises FuzzForgeError: If pull fails.
|
||||
:raises SecPipeError: If pull fails.
|
||||
|
||||
"""
|
||||
client: PodmanClient = self.get_client()
|
||||
@@ -265,7 +265,7 @@ class Podman(AbstractFuzzForgeSandboxEngine):
|
||||
get_logger().info("image pulled successfully", image=image)
|
||||
except Exception as exc:
|
||||
message = f"Failed to pull image '{image}': {exc}"
|
||||
raise FuzzForgeError(message) from exc
|
||||
raise SecPipeError(message) from exc
|
||||
|
||||
def tag_image(self, source: str, target: str) -> None:
|
||||
"""Tag an image with a new name.
|
||||
@@ -524,7 +524,7 @@ class Podman(AbstractFuzzForgeSandboxEngine):
|
||||
|
||||
Creates a temporary container, reads the file, and removes the container.
|
||||
|
||||
:param image: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
|
||||
:param image: Image reference (e.g., "secpipe-rust-analyzer:latest").
|
||||
:param path: Path to file inside image.
|
||||
:returns: File contents as string.
|
||||
|
||||
1
secpipe-common/tests/conftest.py
Normal file
1
secpipe-common/tests/conftest.py
Normal file
@@ -0,0 +1 @@
|
||||
pytest_plugins = ["secpipe_tests.fixtures"]
|
||||
@@ -4,7 +4,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fuzzforge_common.sandboxes.engines.docker.cli import DockerCLI
|
||||
from secpipe_common.sandboxes.engines.docker.cli import DockerCLI
|
||||
|
||||
|
||||
def test_docker_cli_base_cmd() -> None:
|
||||
@@ -9,8 +9,8 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fuzzforge_common.exceptions import FuzzForgeError
|
||||
from fuzzforge_common.sandboxes.engines.podman.cli import PodmanCLI, _is_running_under_snap
|
||||
from secpipe_common.exceptions import SecPipeError
|
||||
from secpipe_common.sandboxes.engines.podman.cli import PodmanCLI, _is_running_under_snap
|
||||
|
||||
|
||||
# Helper to mock Linux platform for testing (since Podman is Linux-only)
|
||||
@@ -60,7 +60,7 @@ def test_snap_detection_when_snap_not_set() -> None:
|
||||
def test_podman_cli_blocks_macos() -> None:
|
||||
"""Test that PodmanCLI raises error on macOS."""
|
||||
with mock.patch.object(sys, "platform", "darwin"):
|
||||
with pytest.raises(FuzzForgeError) as exc_info:
|
||||
with pytest.raises(SecPipeError) as exc_info:
|
||||
PodmanCLI()
|
||||
assert "Podman is not supported on macOS" in str(exc_info.value)
|
||||
assert "Docker" in str(exc_info.value)
|
||||
@@ -8,4 +8,4 @@ WORKDIR /app
|
||||
|
||||
RUN /bin/uv venv && /bin/uv pip install --find-links /wheels $PACKAGE
|
||||
|
||||
CMD [ "/bin/uv", "run", "uvicorn", "fuzzforge_mcp.application:app"]
|
||||
CMD [ "/bin/uv", "run", "uvicorn", "secpipe_mcp.application:app"]
|
||||
@@ -1,10 +1,10 @@
|
||||
# FuzzForge MCP
|
||||
# SecPipe MCP
|
||||
|
||||
Model Context Protocol (MCP) server that enables AI agents to orchestrate FuzzForge security research modules.
|
||||
Model Context Protocol (MCP) server that enables AI agents to orchestrate SecPipe security research modules.
|
||||
|
||||
## Overview
|
||||
|
||||
FuzzForge MCP provides a standardized interface for AI agents (Claude Code, GitHub Copilot, Claude Desktop) to:
|
||||
SecPipe MCP provides a standardized interface for AI agents (Claude Code, GitHub Copilot, Claude Desktop) to:
|
||||
|
||||
- List and discover available security modules
|
||||
- Execute modules in isolated containers
|
||||
@@ -17,20 +17,20 @@ The server communicates with AI agents using the [Model Context Protocol](https:
|
||||
|
||||
### Automatic Installation (Recommended)
|
||||
|
||||
Use the FuzzForge CLI to automatically configure MCP for your AI agent:
|
||||
Use the SecPipe CLI to automatically configure MCP for your AI agent:
|
||||
|
||||
```bash
|
||||
# For GitHub Copilot
|
||||
uv run fuzzforge mcp install copilot
|
||||
uv run secpipe mcp install copilot
|
||||
|
||||
# For Claude Code (VS Code extension)
|
||||
uv run fuzzforge mcp install claude-code
|
||||
uv run secpipe mcp install claude-code
|
||||
|
||||
# For Claude Desktop (standalone app)
|
||||
uv run fuzzforge mcp install claude-desktop
|
||||
uv run secpipe mcp install claude-desktop
|
||||
|
||||
# Verify installation
|
||||
uv run fuzzforge mcp status
|
||||
uv run secpipe mcp status
|
||||
```
|
||||
|
||||
After installation, restart your AI agent to activate the connection.
|
||||
@@ -44,13 +44,13 @@ For custom setups, you can manually configure the MCP server.
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fuzzforge": {
|
||||
"command": "/path/to/fuzzforge_ai/.venv/bin/python",
|
||||
"args": ["-m", "fuzzforge_mcp"],
|
||||
"cwd": "/path/to/fuzzforge_ai",
|
||||
"secpipe": {
|
||||
"command": "/path/to/secpipe_ai/.venv/bin/python",
|
||||
"args": ["-m", "secpipe_mcp"],
|
||||
"cwd": "/path/to/secpipe_ai",
|
||||
"env": {
|
||||
"FUZZFORGE_MODULES_PATH": "/path/to/fuzzforge_ai/fuzzforge-modules",
|
||||
"FUZZFORGE_ENGINE__TYPE": "docker"
|
||||
"SECPIPE_MODULES_PATH": "/path/to/secpipe_ai/secpipe-modules",
|
||||
"SECPIPE_ENGINE__TYPE": "docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,14 +62,14 @@ For custom setups, you can manually configure the MCP server.
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"fuzzforge": {
|
||||
"secpipe": {
|
||||
"type": "stdio",
|
||||
"command": "/path/to/fuzzforge_ai/.venv/bin/python",
|
||||
"args": ["-m", "fuzzforge_mcp"],
|
||||
"cwd": "/path/to/fuzzforge_ai",
|
||||
"command": "/path/to/secpipe_ai/.venv/bin/python",
|
||||
"args": ["-m", "secpipe_mcp"],
|
||||
"cwd": "/path/to/secpipe_ai",
|
||||
"env": {
|
||||
"FUZZFORGE_MODULES_PATH": "/path/to/fuzzforge_ai/fuzzforge-modules",
|
||||
"FUZZFORGE_ENGINE__TYPE": "docker"
|
||||
"SECPIPE_MODULES_PATH": "/path/to/secpipe_ai/secpipe-modules",
|
||||
"SECPIPE_ENGINE__TYPE": "docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,14 +81,14 @@ For custom setups, you can manually configure the MCP server.
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"fuzzforge": {
|
||||
"secpipe": {
|
||||
"type": "stdio",
|
||||
"command": "/path/to/fuzzforge_ai/.venv/bin/python",
|
||||
"args": ["-m", "fuzzforge_mcp"],
|
||||
"cwd": "/path/to/fuzzforge_ai",
|
||||
"command": "/path/to/secpipe_ai/.venv/bin/python",
|
||||
"args": ["-m", "secpipe_mcp"],
|
||||
"cwd": "/path/to/secpipe_ai",
|
||||
"env": {
|
||||
"FUZZFORGE_MODULES_PATH": "/path/to/fuzzforge_ai/fuzzforge-modules",
|
||||
"FUZZFORGE_ENGINE__TYPE": "docker"
|
||||
"SECPIPE_MODULES_PATH": "/path/to/secpipe_ai/secpipe-modules",
|
||||
"SECPIPE_ENGINE__TYPE": "docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,10 +99,10 @@ For custom setups, you can manually configure the MCP server.
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
| -------- | -------- | ------- | ----------- |
|
||||
| `FUZZFORGE_MODULES_PATH` | Yes | - | Path to the modules directory |
|
||||
| `FUZZFORGE_ENGINE__TYPE` | No | `docker` | Container engine (`docker` or `podman`) |
|
||||
| `FUZZFORGE_ENGINE__GRAPHROOT` | No | - | Container storage path (Podman under Snap only) |
|
||||
| `FUZZFORGE_ENGINE__RUNROOT` | No | - | Container runtime state path (Podman under Snap only) |
|
||||
| `SECPIPE_MODULES_PATH` | Yes | - | Path to the modules directory |
|
||||
| `SECPIPE_ENGINE__TYPE` | No | `docker` | Container engine (`docker` or `podman`) |
|
||||
| `SECPIPE_ENGINE__GRAPHROOT` | No | - | Container storage path (Podman under Snap only) |
|
||||
| `SECPIPE_ENGINE__RUNROOT` | No | - | Container runtime state path (Podman under Snap only) |
|
||||
|
||||
## Available Tools
|
||||
|
||||
@@ -110,7 +110,7 @@ The MCP server exposes the following tools to AI agents:
|
||||
|
||||
### Project Management
|
||||
|
||||
- **`init_project`** - Initialize a new FuzzForge project
|
||||
- **`init_project`** - Initialize a new SecPipe project
|
||||
- **`set_project_assets`** - Set initial assets (source code, contracts, etc.) for the project
|
||||
|
||||
### Module Management
|
||||
@@ -135,7 +135,7 @@ The server also provides resources for accessing:
|
||||
|
||||
### From AI Agent (e.g., Claude Code)
|
||||
|
||||
Once configured, AI agents can interact with FuzzForge naturally:
|
||||
Once configured, AI agents can interact with SecPipe naturally:
|
||||
|
||||
```text
|
||||
User: List the available security modules
|
||||
@@ -161,10 +161,10 @@ For testing during development, you can run the MCP server directly:
|
||||
|
||||
```bash
|
||||
# Run MCP server in stdio mode (for AI agents)
|
||||
uv run python -m fuzzforge_mcp
|
||||
uv run python -m secpipe_mcp
|
||||
|
||||
# Run HTTP server for testing (not for production)
|
||||
uv run uvicorn fuzzforge_mcp.application:app --reload
|
||||
uv run uvicorn secpipe_mcp.application:app --reload
|
||||
```
|
||||
|
||||
## Architecture
|
||||
@@ -178,14 +178,14 @@ uv run uvicorn fuzzforge_mcp.application:app --reload
|
||||
│ stdio/JSON-RPC
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ FuzzForge MCP Server │
|
||||
│ SecPipe MCP Server │
|
||||
│ Tools: init_project, list_modules, │
|
||||
│ execute_module, execute_workflow│
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ FuzzForge Runner │
|
||||
│ SecPipe Runner │
|
||||
│ Podman/Docker Orchestration │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
@@ -212,6 +212,6 @@ uv run pytest
|
||||
|
||||
## See Also
|
||||
|
||||
- [FuzzForge Main README](../README.md) - Overall project documentation
|
||||
- [Module SDK](../fuzzforge-modules/fuzzforge-modules-sdk/README.md) - Creating custom modules
|
||||
- [SecPipe Main README](../README.md) - Overall project documentation
|
||||
- [Module SDK](../secpipe-modules/secpipe-modules-sdk/README.md) - Creating custom modules
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user