Self-hosted pentest management workspace for tracking engagements, running tools, auto-importing findings, and generating reports
---
name: pentestcompanion-workspace
description: Self-hosted pentest management workspace for tracking engagements, running tools, auto-importing findings, and generating reports
triggers:
- set up pentest companion for engagement tracking
- create a new pentest engagement in companion
- run nmap scan through pentestcompanion
- import findings from burp or nessus
- generate pentest report from companion
- schedule recurring scans in pentestcompanion
- use pentestcompanion terminal logging
- configure pentestcompanion webhooks
---
# Pentest Companion Workspace
> Skill by [ara.so](https://ara.so) — Security Skills collection.
Pentest Companion is a self-hosted workspace for managing penetration testing engagements. It consolidates target tracking, tool execution, finding management, CVSS scoring, evidence collection, client portals, and report generation into a single interface. All data stays on your infrastructure—no cloud dependencies.
## What It Does
- **Engagement Management**: Track targets, open ports, credentials, attack paths, PTES checklist phases, and time spent
- **Finding Management**: CVSS v3.1 scoring, CVE lookup, evidence uploads, 2400+ templates, bulk operations
- **Tools Hub**: 90+ integrated tools (nmap, gobuster, nikto, sqlmap, netexec, impacket suite, etc.) with live output streaming and auto-import
- **Web Scanner**: Passive security scanner for TLS, headers, cookies, CORS, exposed files, tech fingerprinting
- **Reporting**: DOCX/PDF generation with branded cover pages, executive summaries, and technical findings
- **Workflow Playbooks**: Sequential multi-tool scan pipelines (External Recon, Web App, AD/SMB Enum, etc.)
- **Terminal Logging**: Pipe command output from your terminal into engagement sessions with ANSI replay
- **Scheduled Scans**: Recurring tool runs against targets with auto-import
- **Webhooks**: Slack/Discord/Teams notifications on finding creation
- **REST API**: Read-only endpoints for engagements and findings
## Installation
### Docker (Recommended)
```bash
git clone https://github.com/Poellie01/PentestCompanion.git
cd PentestCompanion
cp .env.example .env
# Generate SECRET_KEY
python3 -c "import secrets; print(secrets.token_hex(32))" | \
xargs -I {} sed -i 's/^SECRET_KEY=$/SECRET_KEY={}/' .env
# Edit .env to set ADMIN_PASSWORD, SMTP settings (optional)
nano .env
docker compose up -d
docker compose logs -f app
```
Access at `http://localhost:5000`. Default admin credentials are printed on first run.
### Python
```bash
git clone https://github.com/Poellie01/PentestCompanion.git
cd PentestCompanion
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.py
```
## Configuration
`.env` file controls all configuration:
```bash
# Required
SECRET_KEY=<generated-hex-key>
ADMIN_PASSWORD=<your-secure-password>
# Database (defaults to SQLite)
DATABASE_URL=sqlite:///pentest_companion.db
# Or PostgreSQL: postgresql://user:pass@localhost/pentestcompanion
# Email (for invites, password reset, MFA)
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=<app-password>
SMTP_FROM=noreply@example.com
# Or Resend
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM=noreply@yourdomain.com
# Application
HOST=0.0.0.0
PORT=5000
FLASK_ENV=production
# SSRF Protection (block private IPs in webhooks/scanner)
SSRF_BLOCK_PRIVATE=true
```
## Core Workflows
### Creating an Engagement
```python
# Via Python API (if extending the app)
from models import Engagement, db
engagement = Engagement(
name="Acme Corp External Pentest",
client="Acme Corporation",
scope="10.0.0.0/24, *.acme.com",
start_date=datetime(2026, 6, 1),
end_date=datetime(2026, 6, 15),
status="in_progress"
)
db.session.add(engagement)
db.session.commit()
```
Via UI: **Engagements → New Engagement** → fill form → optionally enable **Auto-Scan** to run tools on target creation.
### Adding Targets
```python
from models import Target
target = Target(
engagement_id=1,
ip="10.0.0.50",
hostname="web01.acme.com",
os="Linux",
ports="22,80,443"
)
db.session.add(target)
db.session.commit()
```
UI: **Engagement page → Targets → Add Target**
### Running Tools
**From UI:**
1. Navigate to **Tools Hub**
2. Select tool (e.g., `nmap-quick`)
3. Choose engagement and target
4. Click **Run Tool**
5. Watch live output stream
6. Findings auto-import on completion
**From terminal with logging:**
```bash
# Set up pclog helper in ~/.bashrc or ~/.zshrc
PCLOG_TOKEN="pcsk_your_token_here"
PCLOG_BASE="http://localhost:5000"
pclog() {
local eid=$1; shift
local name="${*:-$(date +%H:%M:%S)}"
local sid
sid=$(curl -sf -X POST "$PCLOG_BASE/api/v1/terminal/start" \
-H "Authorization: Bearer $PCLOG_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"engagement_id\":$eid,\"name\":\"$name\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['session_id'])")
while IFS= read -r line; do
printf '%s\n' "$line"
printf '%s\n' "$line" | curl -sf -X POST "$PCLOG_BASE/api/v1/terminal/append/$sid" \
-H "Authorization: Bearer $PCLOG_TOKEN" \
-H "Content-Type: application/octet-stream" --data-binary @- > /dev/null
done
curl -sf -X POST "$PCLOG_BASE/api/v1/terminal/close/$sid" \
-H "Authorization: Bearer $PCLOG_TOKEN" > /dev/null
}
# Usage
nmap -sV -p- 10.0.0.50 | pclog 1 "nmap full scan"
gobuster dir -u http://10.0.0.50 -w /usr/share/wordlists/common.txt | pclog 1 "gobuster"
```
### Creating Findings
```python
from models import Finding
finding = Finding(
engagement_id=1,
title="SQL Injection in Login Form",
severity="critical",
cvss_score=9.8,
cvss_vector="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
status="open",
affected_hosts="web01.acme.com",
description="The login form is vulnerable to SQL injection via the username parameter.",
remediation="Use parameterized queries or prepared statements.",
references="https://owasp.org/www-community/attacks/SQL_Injection"
)
db.session.add(finding)
db.session.commit()
```
**Bulk import from Nessus/Burp:**
UI: **Findings → Import → Upload `.nessus` or Burp XML** → findings auto-created
**Using templates:**
UI: **Findings → New Finding → Use Template** → select from 2400+ templates → customize
### Web Scanning
```python
# Via Python (custom integration)
import requests
response = requests.post(
"http://localhost:5000/api/v1/scanner/scan",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={
"url": "https://example.com",
"deep_scan": True,
"engagement_id": 1
}
)
scan_id = response.json()["scan_id"]
# Poll for results
results = requests.get(
f"http://localhost:5000/api/v1/scanner/results/{scan_id}",
headers={"Authorization": f"Bearer {API_TOKEN}"}
).json()
```
UI: **Web Scanner → New Scan → Enter URL → Run** → optionally promote findings to engagement
### Workflow Playbooks
**Running a playbook:**
```python
# Custom playbook definition (models.py)
from models import Playbook, PlaybookStep
playbook = Playbook(
name="Custom Web Enumeration",
description="Multi-stage web application enumeration",
team_id=1
)
db.session.add(playbook)
db.session.flush()
steps = [
PlaybookStep(playbook_id=playbook.id, order=1, tool_name="whatweb", args=""),
PlaybookStep(playbook_id=playbook.id, order=2, tool_name="nikto", args=""),
PlaybookStep(playbook_id=playbook.id, order=3, tool_name="gobuster-dir", args="-w /usr/share/wordlists/dirb/common.txt"),
PlaybookStep(playbook_id=playbook.id, order=4, tool_name="sqlmap", args="--batch --crawl=2")
]
db.session.add_all(steps)
db.session.commit()
```
UI: **Playbooks → Select Playbook → Choose Target → Run** → watch step-by-step progress
### Scheduled Scans
```python
from models import ScheduledScan
scan = ScheduledScan(
target_id=5,
tool_name="nmap-quick",
interval="daily", # or 'hourly', '6hours', 'weekly'
enabled=True
)
db.session.add(scan)
db.session.commit()
```
UI: **Target page → Scheduled Scans → Add Schedule**
Background daemon runs every 60 seconds and claims due jobs.
### Generating Reports
```python
# Via Python (custom script)
import requests
response = requests.post(
"http://localhost:5000/api/v1/reports/generate",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={
"engagement_id": 1,
"format": "docx", # or 'pdf'
"include_executive_summary": True,
"include_technical_report": True,
"redact_sensitive": False
}
)
with open("report.docx", "wb") as f:
f.write(response.content)
```
UI: **Engagement page → Report → Generate Report** → customize sections → download DOCX/PDF
### Webhooks
```python
from models import Webhook
webhook = Webhook(
team_id=1,
name="Slack Critical Findings",
url="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX",
webhook_type="slack",
enabled=True,
trigger_on_manual=True,
trigger_on_auto_import=True,
severity_filter=["critical", "high"]
)
db.session.add(webhook)
db.session.commit()
```
UI: **Team Settings → Webhooks → New Webhook** → paste URL → test delivery
**Slack payload example:**
```json
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🔴 New Critical Finding"
}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*Title:*\nSQL Injection in Login"},
{"type": "mrkdwn", "text": "*Severity:*\nCritical (9.8)"},
{"type": "mrkdwn", "text": "*Host:*\nweb01.acme.com"},
{"type": "mrkdwn", "text": "*Engagement:*\nAcme Corp Pentest"}
]
}
]
}
```
## REST API
All API requests require a Bearer token (create under **Team Settings → API Tokens**).
### List Engagements
```bash
curl -H "Authorization: Bearer pcsk_your_token" \
http://localhost:5000/api/v1/engagements
```
**Response:**
```json
{
"engagements": [
{
"id": 1,
"name": "Acme Corp External Pentest",
"client": "Acme Corporation",
"status": "in_progress",
"start_date": "2026-06-01",
"end_date": "2026-06-15"
}
]
}
```
### List Findings
```bash
curl -H "Authorization: Bearer pcsk_your_token" \
"http://localhost:5000/api/v1/engagements/1/findings?severity=critical&status=open&page=1"
```
**Response:**
```json
{
"findings": [
{
"id": 42,
"title": "SQL Injection in Login Form",
"severity": "critical",
"cvss_score": 9.8,
"status": "open",
"affected_hosts": "web01.acme.com",
"created_at": "2026-06-02T14:30:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 50,
"total": 1
}
}
```
### Terminal Logging API
**Start session:**
```bash
curl -X POST http://localhost:5000/api/v1/terminal/start \
-H "Authorization: Bearer pcsk_your_token" \
-H "Content-Type: application/json" \
-d '{"engagement_id":1,"name":"nmap scan"}'
```
**Append output:**
```bash
echo "Starting Nmap 7.94" | curl -X POST http://localhost:5000/api/v1/terminal/append/SESSION_ID \
-H "Authorization: Bearer pcsk_your_token" \
-H "Content-Type: application/octet-stream" \
--data-binary @-
```
**Close session:**
```bash
curl -X POST http://localhost:5000/api/v1/terminal/close/SESSION_ID \
-H "Authorization: Bearer pcsk_your_token"
```
## Tool Integration Examples
### Adding a Custom Tool
```python
# tools/custom_tool.py
from tools.base import BaseTool
import subprocess
class MyCustomTool(BaseTool):
name = "my-custom-tool"
display_name = "My Custom Scanner"
category = "Custom"
description = "Custom vulnerability scanner"
def check_installed(self):
return subprocess.run(["which", "my-scanner"],
capture_output=True).returncode == 0
def build_command(self, target, extra_args=""):
return f"my-scanner --target {target.ip} {extra_args}"
def parse_output(self, output):
# Return list of Finding objects
findings = []
if "VULN-001" in output:
findings.append({
"title": "Custom Vulnerability Found",
"severity": "high",
"affected_hosts": target.ip,
"description": "Detailed description..."
})
return findings
```
Register in `tools/__init__.py`:
```python
from tools.custom_tool import MyCustomTool
TOOLS = [
# ... existing tools
MyCustomTool(),
]
```
### Auto-Import Hook
```python
# Custom auto-import handler
from models import Finding, db
def process_tool_output(engagement_id, tool_name, output):
"""Called after tool execution completes"""
tool = get_tool_by_name(tool_name)
parsed = tool.parse_output(output)
for item in parsed:
finding = Finding(
engagement_id=engagement_id,
title=item["title"],
severity=item["severity"],
affected_hosts=item["affected_hosts"],
description=item["description"],
auto_imported=True,
import_source=tool_name
)
db.session.add(finding)
db.session.commit()
# Trigger webhooks
trigger_webhooks(engagement_id, parsed)
```
## Common Patterns
### Bulk Finding Updates
```python
from models import Finding, db
# Mark all informational findings as false positive
findings = Finding.query.filter_by(
engagement_id=1,
severity="informational"
).all()
for finding in findings:
finding.status = "false_positive"
db.session.commit()
```
UI: Select findings → **Bulk Actions → Mark False Positive**
### Export/Import Engagements
**Export:**
```python
from utils.bundle import export_bundle
bundle_path = export_bundle(engagement_id=1, output_dir="/tmp")
# Creates /tmp/acme-corp-pentest-2026-06-02.pcbundle
```
UI: **Engagement → Export Bundle**
**Import:**
```python
from utils.bundle import import_bundle
new_engagement_id = import_bundle("/path/to/bundle.pcbundle")
```
UI: **Engagements → Import Bundle** → upload `.pcbundle`
### Client Portal Sharing
```python
from models import ClientPortal
import secrets
portal = ClientPortal(
engagement_id=1,
token=secrets.token_urlsafe(32),
password_protected=True,
password_hash=generate_password_hash("client-pass"),
expires_at=datetime.now() + timedelta(days=30),
enabled=True
)
db.session.add(portal)
db.session.commit()
# Share URL: http://localhost:5000/portal/{portal.token}
```
UI: **Engagement → Client Portal → Create Portal Link** → copy shareable URL
### Exam Mode
```python
from models import ExamSession
exam = ExamSession(
engagement_id=1,
exam_type="OSCP",
duration_hours=24,
points_required=70,
started_at=datetime.now()
)
db.session.add(exam)
db.session.commit()
```
UI: **Engagement → Exam Mode → Start Exam** → live countdown in navbar → points tracker → screenshot slots
## Troubleshooting
### Tools Not Showing Up
**Problem:** Tool shows as "Not Installed" even though it's on PATH
**Solution:**
```bash
# Verify tool is accessible
docker exec -it pentestcompanion-app-1 which nmap
# Tools are detected via subprocess.run(["which", "tool"])
# Ensure tool binary is in container PATH
# For custom tools, add to Dockerfile or mount volume
```
### Auto-Import Not Working
**Problem:** Tool runs successfully but findings don't appear
**Solution:**
```python
# Check tool has parse_output() method
from tools import get_tool_by_name
tool = get_tool_by_name("nmap-quick")
print(tool.parse_output.__doc__)
# Verify output parsing logic
output = """... tool output ..."""
findings = tool.parse_output(output)
print(findings) # Should return list of dicts
```
Enable debug logging in `.env`:
```bash
FLASK_ENV=development
LOG_LEVEL=DEBUG
```
### Webhook Not Firing
**Problem:** Webhooks configured but no notifications received
**Solution:**
```bash
# Check webhook delivery log in UI
Team Settings → Webhooks → View Deliveries
# Test webhook manually
curl -X POST http://localhost:5000/api/v1/webhooks/test/WEBHOOK_ID \
-H "Authorization: Bearer pcsk_your_token"
# Verify URL is not blocked by SSRF protection
# Private IPs blocked by default (10.x.x.x, 192.168.x.x, 127.x.x.x)
# To allow: SSRF_BLOCK_PRIVATE=false in .env
```
### Database Migration Issues
**Problem:** Schema changes not applied
**Solution:**
```bash
# Apply migrations manually
docker exec -it pentestcompanion-app-1 flask db upgrade
# Or recreate database (WARNING: data loss)
docker compose down -v
docker compose up -d
```
### Report Generation Fails
**Problem:** "Failed to generate report" error
**Solution:**
```bash
# Check pandoc is installed (for PDF conversion)
docker exec -it pentestcompanion-app-1 which pandoc
# Verify template files exist
docker exec -it pentestcompanion-app-1 ls -la templates/report_template.docx
# Check logs for detailed error
docker compose logs app | grep -i report
```
### Performance with Large Engagements
**Problem:** UI sluggish with 1000+ findings
**Solution:**
```python
# Enable pagination in queries
findings = Finding.query.filter_by(engagement_id=1)\
.order_by(Finding.severity.desc())\
.paginate(page=1, per_page=50)
# Archive old engagements
engagement.status = "archived"
db.session.commit()
```
UI: **Engagement → Archive** (hides from main list but preserves data)
## Environment Variables Reference
```bash
# Core
SECRET_KEY=<required-hex-string>
ADMIN_PASSWORD=<optional-overrides-bootstrap>
DATABASE_URL=sqlite:///pentest_companion.db
FLASK_ENV=production
HOST=0.0.0.0
PORT=5000
# Email
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=<email>
SMTP_PASSWORD=<password>
SMTP_FROM=<from-address>
# Or use Resend
RESEND_API_KEY=<key>
RESEND_FROM=<from-address>
# Security
SSRF_BLOCK_PRIVATE=true
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SAMESITE=Lax
# Logging
LOG_LEVEL=INFO
LOG_FILE=/var/log/pentestcompanion.log
# Scheduled Scans
SCHEDULER_INTERVAL=60 # seconds
```
## Key Files
- `app.py` - Flask application entry point
- `models.py` - SQLAlchemy models (Engagement, Finding, Target, etc.)
- `tools/` - Tool integration modules
- `routes/` - Flask blueprints for each feature
- `templates/` - Jinja2 templates for UI
- `static/` - CSS, JS, images
- `utils/bundle.py` - .pcbundle export/import
- `utils/scanner.py` - Web scanner logic
- `utils/report.py` - Report generation (DOCX/PDF)
- `migrations/` - Alembic database migrations
## Additional Resources
- **Documentation**: `docs/` folder in repository
- **Tool Templates**: `templates/finding_templates/` (2400+ pre-written findings)
- **Example Configs**: `.env.example`, `docker-compose.yml`
- **API Spec**: Built-in Swagger UI at `/api/docs` (when enabled)
---
This skill enables AI agents to help users set up, configure, and operate Pentest Companion for comprehensive penetration testing engagement management.
Creator's repository · aradotso/security-skills