pentestcompanion-workspace

Self-hosted pentest management workspace for tracking engagements, running tools, auto-importing findings, and generating reports

Skill file

Preview skill file
---
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.

Source

Creator's repository · aradotso/security-skills

View on GitHub

Security

Security checks in progress
Results will appear here once audits complete
What this skill can do
Reads your filesConnects to the internetRuns code on your machine
Checked by 3 independent security firms
Does it try to trick the AI?Not yet checkedPending · Gen Agent Trust Hub
Does it sneak in hidden code?Not yet checkedPending · Socket
Does it have known bugs?Not yet checkedPending · Snyk