@aradotso/pentestops-dashboard
@aradotso/pentestops-dashboard — AI coding skill
| 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": "[email protected]",
"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": "[email protected]",
"phone": "+1-555-0123",
"website": "https://acme.com",
"industry": "Technology",
"contacts": [
{
"name": "Jane Smith",
"role": "CISO",
"email": "[email protected]",
"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);
Loading...
Select a file to preview
Analyzing security...
Checking scan reports and verification data.
Bill of Materials
Everything this skill can do — files, network, commands, and more.