| 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
git clone https://github.com/0xBugatti/PentestOPS.git
cd PentestOPS
npm install
cd frontend && npm install && cd ..
cd backend && npm install && cd ..
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
docker run -d --name mongodb -p 27017:27017 mongo:latest
npm run dev
Access at:
- Frontend:
http://localhost:3000
- Backend API:
http://localhost:4000
Docker Deployment (Production)
docker build -t pentestops-dashboard:latest .
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
POST /api/auth/register
{
"username": "pentester",
"email": "pentester@example.com",
"password": "SecurePass123!",
"firstName": "John",
"lastName": "Doe"
}
POST /api/auth/login
{
"username": "pentester",
"password": "SecurePass123!"
}
POST /api/auth/refresh
{
"refreshToken": "your-refresh-token"
}
GET /api/auth/profile
Headers: Authorization: Bearer {accessToken}
Projects
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"]
}
GET /api/projects?status=in-progress&search=webapp&sort=createdAt
GET /api/projects/{projectId}
PUT /api/projects/{projectId}
{
"status": "completed",
"progress": 100
}
DELETE /api/projects/{projectId}
Tasks
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"]
}
GET /api/tasks?project={projectId}&status=in-progress&priority=high
PUT /api/tasks/{taskId}
{
"status": "in-progress",
"progress": 50
}
POST /api/tasks/{taskId}/subtasks
{
"title": "Test login form SQL injection",
"completed": false
}
POST /api/tasks/{taskId}/comments
{
"content": "Found SQL injection in username parameter",
"parentComment": "parent-comment-id"
}
Findings
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"]
}
GET /api/findings?project={projectId}&severity=critical&status=open
PUT /api/findings/{findingId}
{
"status": "fixed",
"remediation": "Implemented parameterized queries",
"retestDate": "2024-01-25T00:00:00Z"
}
CWE Database
POST /api/cwes/import
Content-Type: multipart/form-data
file: cwes.csv
GET /api/cwes?search=injection&type=vulnerability
GET /api/cwes/89
Clients
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"
}
GET /api/clients?search=acme
PUT /api/clients/{clientId}
Pages (Checklists/Documentation)
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"
}
}
],
:
},
: ,
: [, , ]
}
/api/pages/owasp-top--checklist
/api/pages/owasp-top--checklist
{
: { }
}
/api/tasks/{taskId}
{
: []
}
File Attachments
POST /api/attachments
Content-Type: multipart/form-data
file: screenshot.png
entityType: finding
entityId: finding-id
GET /api/attachments/{attachmentId}/download
GET /api/attachments/{attachmentId}/view
GET /api/attachments?entityType=finding&entityId={findingId}
Assets
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"
}
GET /api/assets?project={projectId}&type=server
PUT /api/findings/{findingId}
{
"affectedAssets": ["asset-id"]
}
Global Search
GET /api/search?q=sql+injection&type=finding,task&project={projectId}
Frontend Integration
API Client Setup
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json'
}
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
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) {
{
{ data } = axios.(
,
{ refreshToken }
);
.(, data.);
originalRequest.. = ;
(originalRequest);
} (err) {
.();
.();
.. = ;
}
}
}
.(error);
}
);
api;
Creating a Project with Tasks
'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 {
const { data: project } = await api.post('/api/projects', {
name: formData.get('name'),
description: formData.get('description'),
status: 'planning',
: formData.(),
: formData.(),
: formData.(),
: formData.()?.().().( t.())
});
initialTasks = [
{ : , : , : },
{ : , : , : },
{ : , : , : },
{ : , : , : }
];
.(
initialTasks.(
api.(, {
...task,
: project.
})
)
);
router.();
} (error) {
.(, error);
} {
();
}
};
(
);
}
Creating Findings with CWE Lookup
'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 (debounce);
}, [selectedCwe]);
= () => {
finding = {
: formData.(),
: formData.(),
: formData.(),
: ,
: selectedCwe,
: (formData.() ),
: formData.(),
: projectId,
: formData.()?.().(),
: formData.(),
: formData.(),
: formData.()?.().()
};
{
{ data } = api.(, finding);
files = formData.() [];
( file files) {
uploadData = ();
uploadData.(, file);
uploadData.(, );
uploadData.(, data.);
api.(, uploadData, {
: { : }
});
}
data;
} (error) {
.(, error);
error;
}
};
(
);
}
Backend Development
Creating Custom Middleware
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests from this IP, please try again later'
});
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
Custom Finding Model Extension
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'],
:
},
: {
: ,
: [, , , ],
:
},
: { : },
: { : , : , : },
: { : },
: { : .., : , : },
: [{ : .., : }],
: [{ : }],
: { : },
: { : },
: [{ : }],
: [{ : }]
}, {
:
});
.. = (): {
severityScores = {
: ,
: ,
: ,
: ,
:
};
baseScore = severityScores[.];
assetMultiplier = .. > ? : ;
.(baseScore * assetMultiplier, );
};
.. = (): {
.();
};
mongoose.<>(, );
Bulk Import Findings from Scanner Output
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' });
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;
( port host.?.[]?. || []) {
(port.[].. === ) {
service = port.?.[];
findings.({
: ,
: ,
: ,
: ,
: req..,
: [ip],
: [, ]
});
}
}
}
created = .(findings);
fs..(req.!.);
res.({ : created., : created });
} (error) {
res.().({ : });
}
});
router;
Configuration
Environment Variables Reference
NODE_ENV=production
BACKEND_PORT=4000
MONGODB_URI=mongodb://localhost:27017/pentest-dashboard
JWT_SECRET=${JWT_SECRET}
JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
CORS_ORIGIN=https://yourdomain.com
ALLOW_REGISTRATION=false
MAX_FILE_SIZE=10485760
UPLOAD_DIR=/app/uploads
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
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
BACKUP_DIR="/opt/backups/pentestops"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7
mkdir -p $BACKUP_DIR
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
tar -czf $BACKUP_DIR/uploads_${DATE}.tar.gz \
/opt/pentestops/uploads
find $BACKUP_DIR -type f -mtime +$RETENTION_DAYS -delete
echo "Backup completed: $DATE"
Common Patterns
Automated Pentest Workflow
import api from '../lib/api';
async function runAutomatedPentest(projectId: string) {
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']
});
}
const scanResults = await runNucleiScan('https://target.com');
for (const result of scanResults) {
await api.(, {
: result..,
: result..,
: (result..),
: ,
: projectId,
: result..?.[]?.[],
: result..,
: result..
});
}
findings = api.();
report = (findings.);
api.(, {
: ,
: ,
: report,
: projectId
});
}
(): {
: <, > = {
: ,
: ,
: ,
: ,
:
};
mapping[nucleiSeverity] || ;
}
Exporting Pentest Report
import api from '@/lib/api';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
export async function generatePDF(projectId: string): Promise<Blob> {
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();
doc.setFontSize(24);
doc.text('Penetration Testing Report', 20, 30);
doc.setFontSize(16);
doc.text(project.data.name, 20, 45);
doc.setFontSize();
doc.(, , );
doc.();
doc.();