Agent Skills

pentestify-security-report-generator

Interactive pentest report generator with vulnerability tracking, real-time risk statistics, and PDF export in multiple languages

Install

npx skills add https://github.com/reason-machines/security-skills --skill pentestify-security-report-generator
SKILL.md

Pentestify Security Report Generator

Skill by ara.so — Security Skills collection.

Pentestify is an interactive penetration testing report generator that allows security professionals to:

  • Register vulnerabilities using predefined templates or manually
  • Visualize risk statistics in real-time
  • Export structured corporate reports in PDF format
  • Work in bilingual mode (Spanish/English)
  • Persist reports and findings to SQLite database
  • Manage multiple reports with automatic saving

Built with FastAPI backend (async) and Vanilla JavaScript SPA frontend.

Installation

Quick Start with Docker (Recommended)

# Build the image
docker build -t pentestify:latest .

# Run with persistent volume
docker run -d \
  -p 8000:8000 \
  -v pentestify_data:/app/data \
  --name pentestify \
  pentestify:latest

# Access at http://localhost:8000

Manual Installation

# Clone repository
git clone https://github.com/ccyl13/Pentestify.git
cd Pentestify

# Create virtual environment
python3 -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Install Playwright browsers for PDF generation
playwright install chromium

# Run the server
python3 run.py

The application will be available at http://localhost:8000

Architecture Overview

Frontend (SPA)
  ├── index.html (Single Page Application)
  ├── js/app.js (Vanilla JavaScript)
  └── css/styles.css (Pure CSS)
       ↓ HTTP/REST
Backend (FastAPI)
  ├── main.py (API endpoints)
  ├── models.py (SQLAlchemy ORM)
  ├── schemas.py (Pydantic validation)
  └── database.py (SQLite connection)
       ↓
Database (SQLite)
  └── pentestify.db
      ├── reports table
      └── findings table

API Endpoints

Reports Management

Create Report

# POST /api/reports
# Request body
{
  "client_name": "Acme Corp",
  "report_date": "2026-05-21",
  "auditor_name": "John Doe",
  "executive_summary": "Security assessment of web application",
  "scope": "Web application, API endpoints",
  "methodology": "OWASP Testing Guide v4.2",
  "language": "en"  # or "es"
}

Get All Reports

# GET /api/reports
# Response: List of reports with metadata

Get Single Report

# GET /api/reports/{report_id}
# Response: Full report with all findings

Update Report

# PUT /api/reports/{report_id}
# Same body structure as POST

Delete Report

# DELETE /api/reports/{report_id}
# Cascades to delete all associated findings

Findings Management

Add Finding to Report

# POST /api/reports/{report_id}/findings
{
  "title": "SQL Injection in Login Form",
  "severity": "critical",  # critical, high, medium, low, info
  "cvss_score": 9.8,
  "description": "The application is vulnerable to SQL injection...",
  "impact": "Complete database compromise possible",
  "affected_systems": "login.php, /api/auth endpoint",
  "evidence": "Payload: ' OR '1'='1 -- ",
  "remediation": "Use parameterized queries...",
  "references": "CWE-89, OWASP A03:2021",
  "order_index": 0  # Position in report
}

Reorder Findings

# PUT /api/reports/{report_id}/findings/reorder
{
  "finding_ids": [3, 1, 2, 4]  # New order by ID
}

Delete Finding

# DELETE /api/findings/{finding_id}
# Automatically reorders remaining findings

PDF Export

Generate PDF Report

# GET /api/reports/{report_id}/export/pdf
# Returns PDF file download with corporate formatting

Database Backup

Export Database

# GET /api/database/export
# Returns pentestify.db file for backup

Import Database

# POST /api/database/import
# Form-data: file (pentestify.db)

Backend Code Examples

Creating a Custom Vulnerability Template

# backend/models.py extension example
from sqlalchemy import Column, Integer, String, Float, ForeignKey, Text
from sqlalchemy.orm import relationship
from .database import Base

class FindingTemplate(Base):
    """Predefined vulnerability templates"""
    __tablename__ = "finding_templates"
    
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, unique=True, index=True)
    severity = Column(String)
    cvss_base = Column(Float)
    description_template = Column(Text)
    remediation_template = Column(Text)
    cwe_id = Column(String, nullable=True)
    
    def to_finding(self, report_id: int):
        """Convert template to actual finding"""
        return Finding(
            report_id=report_id,
            title=self.name,
            severity=self.severity,
            cvss_score=self.cvss_base,
            description=self.description_template,
            remediation=self.remediation_template,
            references=f"CWE-{self.cwe_id}" if self.cwe_id else ""
        )

Adding Authentication Endpoint

# backend/main.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os

security = HTTPBearer()

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    """Verify API token from environment"""
    expected_token = os.getenv("PENTESTIFY_API_TOKEN")
    if not expected_token:
        raise HTTPException(
            status_code=status.HTTP_501_NOT_IMPLEMENTED,
            detail="Authentication not configured"
        )
    if credentials.credentials != expected_token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication token"
        )
    return credentials.credentials

# Protected endpoint example
@app.post("/api/reports", dependencies=[Depends(verify_token)])
async def create_report_protected(report: ReportCreate, db: Session = Depends(get_db)):
    # Existing logic
    pass

Custom PDF Styling

# backend/main.py - Modify PDF generation
from playwright.async_api import async_playwright

async def generate_pdf_custom(report_html: str, output_path: str):
    """Generate PDF with custom styling"""
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        
        # Inject custom CSS
        custom_css = """
        <style>
            @page { 
                size: A4; 
                margin: 20mm;
                @top-center {
                    content: "CONFIDENTIAL - Internal Use Only";
                    color: red;
                    font-size: 10pt;
                }
            }
            body { 
                font-family: 'Arial', sans-serif;
                line-height: 1.6;
            }
            .severity-critical { 
                background: #dc3545;
                color: white;
                padding: 8px;
                border-radius: 4px;
            }
        </style>
        """
        
        full_html = custom_css + report_html
        await page.set_content(full_html)
        
        await page.pdf(
            path=output_path,
            format='A4',
            print_background=True,
            display_header_footer=True,
            header_template='<div style="font-size:10px; text-align:center; width:100%;">Pentestify Report</div>',
            footer_template='<div style="font-size:10px; text-align:center; width:100%;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>'
        )
        
        await browser.close()

Frontend Integration Examples

Calling the API from JavaScript

// js/app.js example patterns

// Create new report
async function createReport(reportData) {
    try {
        const response = await fetch('/api/reports', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(reportData)
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const newReport = await response.json();
        console.log('Report created:', newReport.id);
        return newReport;
    } catch (error) {
        console.error('Error creating report:', error);
        throw error;
    }
}

// Add finding with auto-save
async function addFinding(reportId, findingData) {
    const finding = {
        ...findingData,
        order_index: await getNextOrderIndex(reportId)
    };
    
    const response = await fetch(`/api/reports/${reportId}/findings`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(finding)
    });
    
    return await response.json();
}

// Export PDF
async function exportPDF(reportId) {
    const response = await fetch(`/api/reports/${reportId}/export/pdf`);
    const blob = await response.blob();
    
    // Trigger download
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `pentest-report-${reportId}.pdf`;
    document.body.appendChild(a);
    a.click();
    window.URL.revokeObjectURL(url);
    a.remove();
}

// Load report with findings
async function loadReport(reportId) {
    const response = await fetch(`/api/reports/${reportId}`);
    const report = await response.json();
    
    // Populate form fields
    document.getElementById('client-name').value = report.client_name || '';
    document.getElementById('report-date').value = report.report_date || '';
    
    // Load findings ordered by order_index
    const findings = report.findings.sort((a, b) => a.order_index - b.order_index);
    findings.forEach(finding => renderFinding(finding));
}

Implementing Drag-and-Drop Reordering

// Enable drag-and-drop for findings list
function enableFindingsReorder() {
    const findingsList = document.getElementById('findings-list');
    
    findingsList.addEventListener('dragstart', (e) => {
        e.target.classList.add('dragging');
    });
    
    findingsList.addEventListener('dragend', async (e) => {
        e.target.classList.remove('dragging');
        await saveFindingsOrder();
    });
    
    findingsList.addEventListener('dragover', (e) => {
        e.preventDefault();
        const afterElement = getDragAfterElement(findingsList, e.clientY);
        const draggable = document.querySelector('.dragging');
        
        if (afterElement == null) {
            findingsList.appendChild(draggable);
        } else {
            findingsList.insertBefore(draggable, afterElement);
        }
    });
}

async function saveFindingsOrder() {
    const findingElements = document.querySelectorAll('.finding-item');
    const findingIds = Array.from(findingElements).map(el => 
        parseInt(el.dataset.findingId)
    );
    
    const reportId = getCurrentReportId();
    await fetch(`/api/reports/${reportId}/findings/reorder`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ finding_ids: findingIds })
    });
}

Common Patterns

Auto-Save Implementation

// Debounced auto-save for report fields
let autoSaveTimeout;

function setupAutoSave() {
    const formInputs = document.querySelectorAll('#report-form input, #report-form textarea');
    
    formInputs.forEach(input => {
        input.addEventListener('input', () => {
            clearTimeout(autoSaveTimeout);
            autoSaveTimeout = setTimeout(async () => {
                await saveReport();
                showNotification('Report saved', 'success');
            }, 2000); // Save 2 seconds after last change
        });
    });
}

async function saveReport() {
    const reportData = {
        client_name: document.getElementById('client-name').value,
        report_date: document.getElementById('report-date').value,
        auditor_name: document.getElementById('auditor-name').value,
        executive_summary: document.getElementById('executive-summary').value,
        scope: document.getElementById('scope').value,
        methodology: document.getElementById('methodology').value,
        language: document.getElementById('language-select').value
    };
    
    const reportId = getCurrentReportId();
    if (reportId) {
        await fetch(`/api/reports/${reportId}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(reportData)
        });
    }
}

Severity Badge Rendering

function getSeverityBadge(severity) {
    const badges = {
        critical: { color: '#dc3545', label: 'CRITICAL' },
        high: { color: '#fd7e14', label: 'HIGH' },
        medium: { color: '#ffc107', label: 'MEDIUM' },
        low: { color: '#0dcaf0', label: 'LOW' },
        info: { color: '#6c757d', label: 'INFO' }
    };
    
    const badge = badges[severity] || badges.info;
    return `<span style="background: ${badge.color}; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">${badge.label}</span>`;
}

Real-Time Statistics

async function updateStatistics(reportId) {
    const response = await fetch(`/api/reports/${reportId}`);
    const report = await response.json();
    
    const stats = {
        total: report.findings.length,
        critical: report.findings.filter(f => f.severity === 'critical').length,
        high: report.findings.filter(f => f.severity === 'high').length,
        medium: report.findings.filter(f => f.severity === 'medium').length,
        low: report.findings.filter(f => f.severity === 'low').length,
        avgCvss: (report.findings.reduce((sum, f) => sum + f.cvss_score, 0) / report.findings.length).toFixed(1)
    };
    
    document.getElementById('stat-total').textContent = stats.total;
    document.getElementById('stat-critical').textContent = stats.critical;
    document.getElementById('stat-high').textContent = stats.high;
    document.getElementById('stat-avg-cvss').textContent = stats.avgCvss;
}

Testing

Running Tests

# All tests
python -m pytest backend/tests/ -v

# Specific test file
python -m pytest backend/tests/test_reports.py -v

# With coverage
python -m pytest backend/tests/ -v --cov=backend --cov-report=html

# Watch mode during development
pytest-watch backend/tests/

Example Test Cases

# backend/tests/test_findings.py
import pytest
from fastapi.testclient import TestClient

def test_add_finding_to_report(client: TestClient, sample_report_id: int):
    """Test adding a vulnerability finding"""
    finding = {
        "title": "Cross-Site Scripting (XSS)",
        "severity": "high",
        "cvss_score": 7.3,
        "description": "Reflected XSS in search parameter",
        "impact": "Session hijacking, credential theft",
        "affected_systems": "/search?q=<payload>",
        "evidence": "<script>alert(document.cookie)</script>",
        "remediation": "Implement output encoding",
        "references": "CWE-79, OWASP A03:2021",
        "order_index": 0
    }
    
    response = client.post(f"/api/reports/{sample_report_id}/findings", json=finding)
    assert response.status_code == 200
    
    data = response.json()
    assert data["title"] == finding["title"]
    assert data["severity"] == finding["severity"]
    assert "id" in data

def test_reorder_findings(client: TestClient, sample_report_with_findings: dict):
    """Test drag-and-drop finding reordering"""
    report_id = sample_report_with_findings["report_id"]
    finding_ids = sample_report_with_findings["finding_ids"]
    
    # Reverse order
    new_order = list(reversed(finding_ids))
    
    response = client.put(
        f"/api/reports/{report_id}/findings/reorder",
        json={"finding_ids": new_order}
    )
    assert response.status_code == 200
    
    # Verify new order
    report_response = client.get(f"/api/reports/{report_id}")
    findings = report_response.json()["findings"]
    
    for idx, finding in enumerate(sorted(findings, key=lambda x: x["order_index"])):
        assert finding["id"] == new_order[idx]

Configuration

Environment Variables

# .env file (not included in repo)
PENTESTIFY_API_TOKEN=your_secret_token_here
PENTESTIFY_DB_PATH=/app/data/pentestify.db
PENTESTIFY_HOST=0.0.0.0
PENTESTIFY_PORT=8000
PENTESTIFY_LOG_LEVEL=info

Docker Volume Management

# Create named volume for persistence
docker volume create pentestify_data

# Backup database
docker run --rm \
  -v pentestify_data:/data \
  -v $(pwd):/backup \
  alpine cp /data/pentestify.db /backup/pentestify_backup.db

# Restore database
docker run --rm \
  -v pentestify_data:/data \
  -v $(pwd):/backup \
  alpine cp /backup/pentestify_backup.db /data/pentestify.db

Custom Port Binding

# Run on different port
docker run -d -p 9000:8000 -v pentestify_data:/app/data pentestify:latest

# Access at http://localhost:9000

Troubleshooting

PDF Generation Fails

Issue: Error generating PDF: Playwright browser not found

Solution:

# Reinstall Playwright browsers
playwright install chromium

# If in Docker, rebuild image
docker build --no-cache -t pentestify:latest .

Database Locked Error

Issue: sqlite3.OperationalError: database is locked

Solution:

# backend/database.py - Increase timeout
engine = create_engine(
    SQLALCHEMY_DATABASE_URL,
    connect_args={"check_same_thread": False, "timeout": 30}
)

Findings Not Reordering

Issue: Drag-and-drop doesn't persist order

Solution: Check order_index initialization

# Ensure all findings have order_index set
async def fix_missing_order_indexes(db: Session, report_id: int):
    findings = db.query(Finding).filter(Finding.report_id == report_id).all()
    for idx, finding in enumerate(findings):
        if finding.order_index is None:
            finding.order_index = idx
    db.commit()

CORS Issues in Development

Issue: Frontend can't connect to backend

Solution:

# backend/main.py
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "http://127.0.0.1:8000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Reports Not Saving

Issue: Auto-save doesn't work

Solution: Check browser console for errors

// Add error handling
async function saveReport() {
    try {
        // ... save logic
    } catch (error) {
        console.error('Save failed:', error);
        showNotification('Failed to save report', 'error');
    }
}

Integration Examples

CI/CD Pipeline

# .github/workflows/test.yml
name: Test Pentestify

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.9'
      
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          playwright install chromium
      
      - name: Run tests
        run: python -m pytest backend/tests/ -v --cov=backend
      
      - name: Build Docker image
        run: docker build -t pentestify:test .

Nginx Reverse Proxy

# /etc/nginx/sites-available/pentestify
server {
    listen 80;
    server_name pentest.example.com;
    
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    # Increase timeout for PDF generation
    location /api/reports/*/export/pdf {
        proxy_pass http://localhost:8000;
        proxy_read_timeout 120s;
    }
}

This skill provides comprehensive coverage of Pentestify's capabilities for AI coding agents to assist developers in generating professional security reports.

Related skills

azure-compliancemicrosoft606KRun Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.firebase-security-rules-auditorfirebase124KAudits Firebase (Firestore, Cloud Storage) security rules for vulnerabilities, privilege escalation, role bypasses, create vs update inconsistencies, resource exhaustion, type safety, size limits, and hasOnly ownership checks. Use when auditing/reviewing rules, running red-team rule assessments, or scoring against auditor checklists. Don't use for Firebase CLI (login, deploy), Auth, Crashlytics, Remote Config, or database queries.browser-fingerprint-auditliarjsdev69KAudit a browser fingerprint for internal contradictions with the liarjs CLI - canvas, WebGL, WebGL2, WebGPU, audio, 220 fonts, WebRTC and timezone probes, scored against the TLS/HTTP/ASN view of the same request. Use when asked to run a browser fingerprint test, see what a fingerprint looks like, check canvas or WebGL fingerprint stability, compare a spoofed profile against a real browser, or find out whether a browser profile is self-consistent.cloudflare-onecloudflare66KDesign, configure, troubleshoot, or review Cloudflare One Zero Trust and SASE deployments. Use cloudflare-one-migrations for migration planning from other vendors.

Search skills and MCP servers

Fuzzy search across 23,137 skills and servers