name: pentestops-dashboard description: Comprehensive penetration testing operations dashboard for managing projects, tasks, findings, clients, and assets with Next.js and MongoDB triggers: - "set up pentestops dashboard" - "create pentest project management system" - "configure pentestops with docker" - "add security findings to pentestops" - "manage penetration testing tasks" - "deploy pentestops dashboard" - "integrate cwe database with pentestops" - "create pentest checklist pages"
PentestOPS Dashboard Skill
Skill by ara.so — Security Skills collection.
Overview
PentestOPS Dashboard is a comprehensive penetration testing operations platform built with Next.js, Express, and MongoDB. It provides project management, task tracking (Kanban/table views), finding management with CWE integration, client management, asset tracking, rich text pages with Editor.js, checklists, comments, file attachments, version history, and global search.
Key Features:
- Full-stack TypeScript application with JWT authentication
- Rich text editor with Notion-like features
- Single Docker container deployment (includes MongoDB, backend, frontend)
- CWE database integration for security findings
- File upload support (PDF, DOCX, XLSX, ZIP, images)
- Threaded comments and version control
Installation
Local Development Setup
# Clone repository
git clone https://github.com/0xBugatti/PentestOPS.git
cd PentestOPS
# Install dependencies
npm install
cd frontend && npm install && cd ..
cd backend && npm install && cd ..
# Create .env file in root
cat > .env << 'EOF'
NODE_ENV=development
BACKEND_PORT=4000
MONGODB_URI=mongodb://localhost:27017/pentest-dashboard
JWT_SECRET=$(openssl rand -base64 32)
JWT_REFRESH_SECRET=$(openssl rand -base64 32)
CORS_ORIGIN=http://localhost:3000
ALLOW_REGISTRATION=true
MAX_FILE_SIZE=10485760
UPLOAD_DIR=./backend/uploads
NEXT_PUBLIC_API_URL=http://localhost:4000
EOF
# Start MongoDB
docker run -d --name mongodb -p 27017:27017 mongo:latest
# Start development servers
npm run dev
Access at:
- Frontend:
http://localhost:3000 - Backend API:
http://localhost:4000
Docker Deployment (Production)
# Build image
docker build -t pentestops-dashboard:latest .
# Run with environment file
docker run -d \
--name pentestops \
--restart unless-stopped \
-p 3000:3000 \
-p 4000:4000 \
-v pentestops-data:/data/db \
-v pentestops-uploads:/app/uploads \
-e JWT_SECRET=${JWT_SECRET} \
-e JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} \
-e NODE_ENV=production \
-e CORS_ORIGIN=https://yourdomain.com \
-e ALLOW_REGISTRATION=false \
pentestops-dashboard:latest
Core API Endpoints
Authentication
// Register new user
POST /api/auth/register
{
"username": "pentester",
"email": "pentester@example.com",
"password": "SecurePass123!",
"firstName": "John",
"lastName": "Doe"
}
// Login
POST /api/auth/login
{
"username": "pentester",
"password": "SecurePass123!"
}
// Returns: { accessToken, refreshToken, user }
// Refresh token
POST /api/auth/refresh
{
"refreshToken": "your-refresh-token"
}
// Get profile
GET /api/auth/profile
Headers: Authorization: Bearer {accessToken}
Projects
// Create project
POST /api/projects
{
"name": "Web Application Security Assessment",
"description": "Comprehensive security audit of client web application",
"status": "in-progress",
"startDate": "2024-01-15T00:00:00Z",
"endDate": "2024-02-15T00:00:00Z",
"client": "client-id",
"tags": ["webapp", "owasp", "critical"]
}
// List projects with filters
GET /api/projects?status=in-progress&search=webapp&sort=createdAt
// Get project details
GET /api/projects/{projectId}
// Update project
PUT /api/projects/{projectId}
{
"status": "completed",
"progress": 100
}
// Delete project
DELETE /api/projects/{projectId}
Tasks
// Create task
POST /api/tasks
{
"title": "SQL Injection Testing",
"description": "Test all input fields for SQL injection vulnerabilities",
"status": "todo",
"priority": "high",
"project": "project-id",
"assignee": "user-id",
"dueDate": "2024-01-20T00:00:00Z",
"tags": ["sqli", "webapp", "owasp-a03"],
"checklist": ["Check login form", "Test search parameters", "Verify API endpoints"]
}
// List tasks with filters
GET /api/tasks?project={projectId}&status=in-progress&priority=high
// Update task status
PUT /api/tasks/{taskId}
{
"status": "in-progress",
"progress": 50
}
// Add subtask
POST /api/tasks/{taskId}/subtasks
{
"title": "Test login form SQL injection",
"completed": false
}
// Add comment to task
POST /api/tasks/{taskId}/comments
{
"content": "Found SQL injection in username parameter",
"parentComment": "parent-comment-id" // Optional for threading
}
Findings
// Create finding
POST /api/findings
{
"title": "SQL Injection in Login Form",
"description": "The login form is vulnerable to SQL injection attacks",
"severity": "critical",
"status": "open",
"cweId": "CWE-89",
"cvssScore": 9.8,
"cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"project": "project-id",
"affectedAssets": ["asset-id"],
"steps": [
"Navigate to login page",
"Enter ' OR '1'='1 in username field",
"Observe authentication bypass"
],
"impact": "Attackers can bypass authentication and gain unauthorized access",
"recommendation": "Use parameterized queries or prepared statements",
"references": ["https://owasp.org/www-community/attacks/SQL_Injection"],
"tags": ["sqli", "authentication", "critical"]
}
// List findings
GET /api/findings?project={projectId}&severity=critical&status=open
// Update finding
PUT /api/findings/{findingId}
{
"status": "fixed",
"remediation": "Implemented parameterized queries",
"retestDate": "2024-01-25T00:00:00Z"
}
CWE Database
// Import CWE database from CSV
POST /api/cwes/import
Content-Type: multipart/form-data
file: cwes.csv
// Search CWEs
GET /api/cwes?search=injection&type=vulnerability
// Get CWE details
GET /api/cwes/89
// Returns CWE-89 (SQL Injection) details
Clients
// Create client
POST /api/clients
{
"name": "Acme Corporation",
"email": "security@acme.com",
"phone": "+1-555-0123",
"website": "https://acme.com",
"industry": "Technology",
"contacts": [
{
"name": "Jane Smith",
"role": "CISO",
"email": "jane.smith@acme.com",
"phone": "+1-555-0124"
}
],
"notes": "Primary contact for all security assessments"
}
// List clients
GET /api/clients?search=acme
// Update client
PUT /api/clients/{clientId}
Pages (Checklists/Documentation)
// Create page with Editor.js content
POST /api/pages
{
"title": "OWASP Top 10 Testing Checklist",
"slug": "owasp-top-10-checklist",
"content": {
"time": 1640995200000,
"blocks": [
{
"type": "header",
"data": {
"text": "OWASP Top 10 Testing Checklist",
"level": 1
}
},
{
"type": "paragraph",
"data": {
"text": "Comprehensive checklist for testing OWASP Top 10 vulnerabilities"
}
},
{
"type": "checklist",
"data": {
"items": [
{
"text": "A01:2021 - Broken Access Control",
"checked": false
},
{
"text": "A02:2021 - Cryptographic Failures",
"checked": false
},
{
"text": "A03:2021 - Injection",
"checked": true
}
]
}
},
{
"type": "code",
"data": {
"code": "' OR '1'='1' --",
"language": "sql"
}
}
],
"version": "2.28.0"
},
"isPublic": false,
"tags": ["owasp", "checklist", "webapp"]
}
// Get page by slug
GET /api/pages/owasp-top-10-checklist
// Update page
PUT /api/pages/owasp-top-10-checklist
{
"content": { /* updated Editor.js blocks */ }
}
// Link page to task
PUT /api/tasks/{taskId}
{
"linkedPages": ["owasp-top-10-checklist"]
}
File Attachments
// Upload file
POST /api/attachments
Content-Type: multipart/form-data
file: screenshot.png
entityType: finding
entityId: finding-id
// Download file
GET /api/attachments/{attachmentId}/download
// View image
GET /api/attachments/{attachmentId}/view
// List attachments for entity
GET /api/attachments?entityType=finding&entityId={findingId}
Assets
// Create asset
POST /api/assets
{
"name": "Web Server - Production",
"type": "server",
"ipAddress": "192.168.1.100",
"hostname": "web-prod-01.acme.com",
"os": "Ubuntu 22.04 LTS",
"ports": [
{
"port": 443,
"protocol": "tcp",
"service": "https",
"version": "nginx/1.18.0"
}
],
"vulnerabilities": ["CVE-2023-1234"],
"project": "project-id",
"notes": "Primary web server for production environment"
}
// List assets
GET /api/assets?project={projectId}&type=server
// Link asset to finding
PUT /api/findings/{findingId}
{
"affectedAssets": ["asset-id"]
}
Global Search
// Search across all entities
GET /api/search?q=sql+injection&type=finding,task&project={projectId}
Frontend Integration
API Client Setup
// lib/api.ts
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor for auth token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor for token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
try {
const { data } = await axios.post(
`${process.env.NEXT_PUBLIC_API_URL}/api/auth/refresh`,
{ refreshToken }
);
localStorage.setItem('accessToken', data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return api(originalRequest);
} catch (err) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
}
return Promise.reject(error);
}
);
export default api;
Creating a Project with Tasks
// app/projects/create/page.tsx
'use client';
import { useState } from 'react';
import api from '@/lib/api';
import { useRouter } from 'next/navigation';
export default function CreateProject() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const formData = new FormData(e.currentTarget);
try {
// Create project
const { data: project } = await api.post('/api/projects', {
name: formData.get('name'),
description: formData.get('description'),
status: 'planning',
startDate: formData.get('startDate'),
endDate: formData.get('endDate'),
client: formData.get('clientId'),
tags: formData.get('tags')?.toString().split(',').map(t => t.trim())
});
// Create initial tasks
const initialTasks = [
{ title: 'Reconnaissance', status: 'todo', priority: 'high' },
{ title: 'Vulnerability Scanning', status: 'todo', priority: 'high' },
{ title: 'Manual Testing', status: 'todo', priority: 'medium' },
{ title: 'Report Writing', status: 'todo', priority: 'low' }
];
await Promise.all(
initialTasks.map(task =>
api.post('/api/tasks', {
...task,
project: project._id
})
)
);
router.push(`/projects/${project._id}`);
} catch (error) {
console.error('Failed to create project:', error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}
Creating Findings with CWE Lookup
// components/FindingForm.tsx
'use client';
import { useState, useEffect } from 'react';
import api from '@/lib/api';
interface CWE {
id: string;
name: string;
description: string;
}
export default function FindingForm({ projectId }: { projectId: string }) {
const [cwes, setCwes] = useState<CWE[]>([]);
const [selectedCwe, setSelectedCwe] = useState<string>('');
useEffect(() => {
const searchCWEs = async (query: string) => {
if (query.length < 2) return;
const { data } = await api.get(`/api/cwes?search=${query}`);
setCwes(data.cwes);
};
const debounce = setTimeout(() => searchCWEs(selectedCwe), 300);
return () => clearTimeout(debounce);
}, [selectedCwe]);
const handleSubmit = async (formData: FormData) => {
const finding = {
title: formData.get('title'),
description: formData.get('description'),
severity: formData.get('severity'),
status: 'open',
cweId: selectedCwe,
cvssScore: parseFloat(formData.get('cvssScore') as string),
cvssVector: formData.get('cvssVector'),
project: projectId,
steps: formData.get('steps')?.toString().split('\n'),
impact: formData.get('impact'),
recommendation: formData.get('recommendation'),
references: formData.get('references')?.toString().split('\n')
};
try {
const { data } = await api.post('/api/findings', finding);
// Upload proof of concept screenshots
const files = formData.getAll('screenshots') as File[];
for (const file of files) {
const uploadData = new FormData();
uploadData.append('file', file);
uploadData.append('entityType', 'finding');
uploadData.append('entityId', data._id);
await api.post('/api/attachments', uploadData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
return data;
} catch (error) {
console.error('Failed to create finding:', error);
throw error;
}
};
return (
<div>
<input
type="text"
placeholder="Search CWE (e.g., SQL Injection)"
value={selectedCwe}
onChange={(e) => setSelectedCwe(e.target.value)}
/>
{cwes.length > 0 && (
<ul>
{cwes.map(cwe => (
<li key={cwe.id} onClick={() => setSelectedCwe(cwe.id)}>
{cwe.id}: {cwe.name}
</li>
))}
</ul>
)}
{/* Rest of form */}
</div>
);
}
Backend Development
Creating Custom Middleware
// backend/src/middleware/rateLimiter.ts
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later'
});
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Limit auth attempts
skipSuccessfulRequests: true
});
// Usage in routes
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
Custom Finding Model Extension
// backend/src/models/Finding.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IFinding extends Document {
title: string;
severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
cweId: string;
cvssScore: number;
project: mongoose.Types.ObjectId;
calculateRiskScore(): number;
generateReport(): string;
}
const FindingSchema = new Schema<IFinding>({
title: { type: String, required: true },
description: { type: String, required: true },
severity: {
type: String,
enum: ['critical', 'high', 'medium', 'low', 'info'],
required: true
},
status: {
type: String,
enum: ['open', 'fixed', 'accepted', 'false-positive'],
default: 'open'
},
cweId: { type: String },
cvssScore: { type: Number, min: 0, max: 10 },
cvssVector: { type: String },
project: { type: Schema.Types.ObjectId, ref: 'Project', required: true },
affectedAssets: [{ type: Schema.Types.ObjectId, ref: 'Asset' }],
steps: [{ type: String }],
impact: { type: String },
recommendation: { type: String },
references: [{ type: String }],
tags: [{ type: String }]
}, {
timestamps: true
});
// Calculate risk score based on severity and affected assets
FindingSchema.methods.calculateRiskScore = function(): number {
const severityScores = {
critical: 10,
high: 7.5,
medium: 5,
low: 2.5,
info: 0
};
const baseScore = severityScores[this.severity];
const assetMultiplier = this.affectedAssets.length > 0 ? 1.2 : 1;
return Math.min(baseScore * assetMultiplier, 10);
};
// Generate markdown report
FindingSchema.methods.generateReport = function(): string {
return `
# ${this.title}
**Severity:** ${this.severity.toUpperCase()}
**CVSS Score:** ${this.cvssScore || 'N/A'}
**CWE:** ${this.cweId || 'N/A'}
**Status:** ${this.status}
## Description
${this.description}
## Steps to Reproduce
${this.steps.map((step, i) => `${i + 1}. ${step}`).join('\n')}
## Impact
${this.impact}
## Recommendation
${this.recommendation}
## References
${this.references.map(ref => `- ${ref}`).join('\n')}
`.trim();
};
export default mongoose.model<IFinding>('Finding', FindingSchema);
Bulk Import Findings from Scanner Output
// backend/src/routes/findings.ts
import express from 'express';
import Finding from '../models/Finding';
import multer from 'multer';
import xml2js from 'xml2js';
const router = express.Router();
const upload = multer({ dest: '/tmp/uploads' });
// Import Nmap XML
router.post('/import/nmap', upload.single('file'), async (req, res) => {
try {
const xmlData = await fs.promises.readFile(req.file!.path, 'utf-8');
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(xmlData);
const findings = [];
for (const host of result.nmaprun.host || []) {
const ip = host.address[0].$.addr;
for (const port of host.ports?.[0]?.port || []) {
if (port.state[0].$.state === 'open') {
const service = port.service?.[0];
findings.push({
title: `Open Port ${port.$.portid}/${port.$.protocol}`,
description: `Service: ${service?.$.name || 'unknown'} (${service?.$.product || 'N/A'})`,
severity: 'info',
status: 'open',
project: req.body.projectId,
affectedAssets: [ip],
tags: ['port-scan', 'nmap']
});
}
}
}
const created = await Finding.insertMany(findings);
await fs.promises.unlink(req.file!.path);
res.json({ imported: created.length, findings: created });
} catch (error) {
res.status(500).json({ error: 'Failed to import Nmap results' });
}
});
export default router;
Configuration
Environment Variables Reference
# Backend Configuration
NODE_ENV=production # development | production
BACKEND_PORT=4000 # API server port
MONGODB_URI=mongodb://localhost:27017/pentest-dashboard # MongoDB connection string
JWT_SECRET=${JWT_SECRET} # JWT signing secret (use env var)
JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET} # Refresh token secret (use env var)
CORS_ORIGIN=https://yourdomain.com # Allowed origins (comma-separated)
ALLOW_REGISTRATION=false # Enable/disable public registration
MAX_FILE_SIZE=10485760 # Max upload size in bytes (10MB)
UPLOAD_DIR=/app/uploads # File storage directory
# Frontend Configuration
NEXT_PUBLIC_API_URL=https://api.yourdomain.com # Backend API URL
Nginx Reverse Proxy Configuration
# /etc/nginx/sites-available/pentestops
upstream backend {
server localhost:4000;
}
upstream frontend {
server localhost:3000;
}
server {
listen 443 ssl http2;
server_name pentestops.example.com;
ssl_certificate /etc/letsencrypt/live/pentestops.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pentestops.example.com/privkey.pem;
# Frontend
location / {
proxy_pass http://frontend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Backend API
location /api {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10M;
}
# WebSocket support (if needed)
location /socket.io {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
MongoDB Backup Script
#!/bin/bash
# /opt/pentestops/backup.sh
BACKUP_DIR="/opt/backups/pentestops"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
mkdir -p $BACKUP_DIR
# Backup MongoDB
docker exec pentestops mongodump \
--archive=/tmp/backup_${DATE}.archive \
--db=pentest-dashboard \
--gzip
docker cp pentestops:/tmp/backup_${DATE}.archive \
$BACKUP_DIR/mongodb_${DATE}.archive
# Backup uploads
tar -czf $BACKUP_DIR/uploads_${DATE}.tar.gz \
/opt/pentestops/uploads
# Remove old backups
find $BACKUP_DIR -type f -mtime +$RETENTION_DAYS -delete
# Optional: Upload to S3
# aws s3 sync $BACKUP_DIR s3://your-bucket/pentestops-backups/
echo "Backup completed: $DATE"
Common Patterns
Automated Pentest Workflow
// scripts/automated-pentest.ts
import api from '../lib/api';
async function runAutomatedPentest(projectId: string) {
// 1. Create reconnaissance tasks
const reconTasks = [
{ title: 'DNS Enumeration', command: 'dnsenum domain.com' },
{ title: 'Subdomain Discovery', command: 'subfinder -d domain.com' },
{ title: 'Port Scanning', command: 'nmap -sV -A target.com' }
];
for (const task of reconTasks) {
await api.post('/api/tasks', {
title: task.title,
description: task.command,
project: projectId,
status: 'todo',
tags: ['automated', 'recon']
});
}
// 2. Run vulnerability scanners
const scanResults = await runNucleiScan('https://target.com');
// 3. Import findings
for (const result of scanResults) {
await api.post('/api/findings', {
title: result.info.name,
description: result.info.description,
severity: mapSeverity(result.info.severity),
status: 'open',
project: projectId,
cweId: result.info.classification?.['cwe-id']?.[0],
tags: result.info.tags,
references: result.info.reference
});
}
// 4. Generate initial report
const findings = await api.get(`/api/findings?project=${projectId}`);
const report = generateExecutiveSummary(findings.data);
await api.post('/api/pages', {
title: `Automated Scan Report - ${new Date().toLocaleDateString()}`,
slug: `scan-report-${Date.now()}`,
content: report,
project: projectId
});
}
function mapSeverity(nucleiSeverity: string): string {
const mapping: Record<string, string> = {
'critical': 'critical',
'high': 'high',
'medium': 'medium',
'low': 'low',
'info': 'info'
};
return mapping[nucleiSeverity] || 'info';
}
Exporting Pentest Report
// utils/reportGenerator.ts
import api from '@/lib/api';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
export async function generatePDF(projectId: string): Promise<Blob> {
// Fetch project data
const [project, findings, tasks] = await Promise.all([
api.get(`/api/projects/${projectId}`),
api.get(`/api/findings?project=${projectId}`),
api.get(`/api/tasks?project=${projectId}`)
]);
const doc = new jsPDF();
// Cover page
doc.setFontSize(24);
doc.text('Penetration Testing Report', 20, 30);
doc.setFontSize(16);
doc.text(project.data.name, 20, 45);
doc.setFontSize(12);
doc.text(`Generated: ${new Date().toLocaleDateString()}`, 20, 55);
// Executive summary
doc.addPage();
doc.setFontSize(18);