| name | pentestify-security-report-generator |
| description | Interactive pentest report generator with vulnerability tracking, real-time risk statistics, and PDF export in multiple languages |
| triggers | ["how do I generate a penetration testing report","create a pentest report with vulnerabilities","export security findings to PDF","use pentestify to document security issues","generate a vulnerability assessment report","how to track pentest findings and export reports","set up pentestify for security reporting","manage vulnerabilities in pentestify"] |
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)
docker build -t pentestify:latest .
docker run -d \
-p 8000:8000 \
-v pentestify_data:/app/data \
--name pentestify \
pentestify:latest
Manual Installation
git clone https://github.com/ccyl13/Pentestify.git
cd Pentestify
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
playwright install chromium
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
{
"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"
}
Get All Reports
Get Single Report
Update Report
Delete Report
Findings Management
Add Finding to Report
{
"title": "SQL Injection in Login Form",
"severity": "critical",
"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
}
Reorder Findings
{
"finding_ids": [3, 1, 2, 4]
}
Delete Finding
PDF Export
Generate PDF Report
Database Backup
Export Database
Import Database
Backend Code Examples
Creating a Custom Vulnerability Template
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
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
@app.post("/api/reports", dependencies=[Depends(verify_token)])
async def create_report_protected(report: ReportCreate, db: Session = Depends(get_db)):
pass
Custom PDF Styling
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()
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
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;
}
}
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();
}
async function exportPDF(reportId) {
const response = await fetch(`/api/reports/${reportId}/export/pdf`);
const blob = await response.blob();
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();
}
async function loadReport(reportId) {
const response = await fetch(`/api/reports/${reportId}`);
const report = await response.json();
document.getElementById('client-name').value = report.client_name || '';
document.getElementById('report-date').value = report.report_date || '';
const findings = report.findings.sort((a, b) => a.order_index - b.order_index);
findings.forEach(finding => renderFinding(finding));
}
Implementing Drag-and-Drop Reordering
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
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);
});
});
}
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
python -m pytest backend/tests/ -v
python -m pytest backend/tests/test_reports.py -v
python -m pytest backend/tests/ -v --cov=backend --cov-report=html
pytest-watch backend/tests/
Example Test Cases
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"]
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
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
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
docker volume create pentestify_data
docker run --rm \
-v pentestify_data:/data \
-v $(pwd):/backup \
alpine cp /data/pentestify.db /backup/pentestify_backup.db
docker run --rm \
-v pentestify_data:/data \
-v $(pwd):/backup \
alpine cp /backup/pentestify_backup.db /data/pentestify.db
Custom Port Binding
docker run -d -p 9000:8000 -v pentestify_data:/app/data pentestify:latest
Troubleshooting
PDF Generation Fails
Issue: Error generating PDF: Playwright browser not found
Solution:
playwright install chromium
docker build --no-cache -t pentestify:latest .
Database Locked Error
Issue: sqlite3.OperationalError: database is locked
Solution:
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
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:
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
async function saveReport() {
try {
} catch (error) {
console.error('Save failed:', error);
showNotification('Failed to save report', 'error');
}
}
Integration Examples
CI/CD Pipeline
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.