# Pentestcompanion Workspace

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

- Skill: `aradotso-security-skills/pentestcompanion-workspace` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aradotso-security-skills/pentestcompanion-workspace`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aradotso-security-skills/pentestcompanion-workspace/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: aradotso (https://skillmd.com/u/aradotso-security-skills)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/aradotso-security-skills/pentestcompanion-workspace

---


# 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.

