| 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 — 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)
git clone https://github.com/Poellie01/PentestCompanion.git
cd PentestCompanion
cp .env.example .env
python3 -c "import secrets; print(secrets.token_hex(32))" | \
xargs -I {} sed -i 's/^SECRET_KEY=$/SECRET_KEY={}/' .env
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
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:
SECRET_KEY=<generated-hex-key>
ADMIN_PASSWORD=<your-secure-password>
DATABASE_URL=sqlite:///pentest_companion.db
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=<app-password>
SMTP_FROM=noreply@example.com
RESEND_API_KEY=re_xxxxxxxxxxxx
RESEND_FROM=noreply@yourdomain.com
HOST=0.0.0.0
PORT=5000
FLASK_ENV=production
SSRF_BLOCK_PRIVATE=true
Core Workflows
Creating an Engagement
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
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:
- Navigate to Tools Hub
- Select tool (e.g.,
nmap-quick)
- Choose engagement and target
- Click Run Tool
- Watch live output stream
- Findings auto-import on completion
From terminal with logging:
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
}
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
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
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"]
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:
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
from models import ScheduledScan
scan = ScheduledScan(
target_id=5,
tool_name="nmap-quick",
interval="daily",
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
import requests
response = requests.post(
"http://localhost:5000/api/v1/reports/generate",
headers={"Authorization": f"Bearer {API_TOKEN}"},
json={
"engagement_id": 1,
"format": "docx",
"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
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:
{
"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
curl -H "Authorization: Bearer pcsk_your_token" \
http://localhost:5000/api/v1/engagements
Response:
{
"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
curl -H "Authorization: Bearer pcsk_your_token" \
"http://localhost:5000/api/v1/engagements/1/findings?severity=critical&status=open&page=1"
Response:
{
"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:
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:
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:
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
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):
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:
from tools.custom_tool import MyCustomTool
TOOLS = [
MyCustomTool(),
]
Auto-Import Hook
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(engagement_id, parsed)
Common Patterns
Bulk Finding Updates
from models import Finding, db
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:
from utils.bundle import export_bundle
bundle_path = export_bundle(engagement_id=1, output_dir="/tmp")
UI: Engagement → Export Bundle
Import:
from utils.bundle import import_bundle
new_engagement_id = import_bundle("/path/to/bundle.pcbundle")
UI: Engagements → Import Bundle → upload .pcbundle
Client Portal Sharing
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()
UI: Engagement → Client Portal → Create Portal Link → copy shareable URL
Exam Mode
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:
docker exec -it pentestcompanion-app-1 which nmap
Auto-Import Not Working
Problem: Tool runs successfully but findings don't appear
Solution:
from tools import get_tool_by_name
tool = get_tool_by_name("nmap-quick")
print(tool.parse_output.__doc__)
output = """... tool output ..."""
findings = tool.parse_output(output)
print(findings)
Enable debug logging in .env:
FLASK_ENV=development
LOG_LEVEL=DEBUG
Webhook Not Firing
Problem: Webhooks configured but no notifications received
Solution:
Team Settings → Webhooks → View Deliveries
curl -X POST http://localhost:5000/api/v1/webhooks/test/WEBHOOK_ID \
-H "Authorization: Bearer pcsk_your_token"
Database Migration Issues
Problem: Schema changes not applied
Solution:
docker exec -it pentestcompanion-app-1 flask db upgrade
docker compose down -v
docker compose up -d
Report Generation Fails
Problem: "Failed to generate report" error
Solution:
docker exec -it pentestcompanion-app-1 which pandoc
docker exec -it pentestcompanion-app-1 ls -la templates/report_template.docx
docker compose logs app | grep -i report
Performance with Large Engagements
Problem: UI sluggish with 1000+ findings
Solution:
findings = Finding.query.filter_by(engagement_id=1)\
.order_by(Finding.severity.desc())\
.paginate(page=1, per_page=50)
engagement.status = "archived"
db.session.commit()
UI: Engagement → Archive (hides from main list but preserves data)
Environment Variables Reference
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
SMTP_SERVER=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=<email>
SMTP_PASSWORD=<password>
SMTP_FROM=<from-address>
RESEND_API_KEY=<key>
RESEND_FROM=<from-address>
SSRF_BLOCK_PRIVATE=true
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SAMESITE=Lax
LOG_LEVEL=INFO
LOG_FILE=/var/log/pentestcompanion.log
SCHEDULER_INTERVAL=60
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.