pentestops-dashboard

star 1

Comprehensive penetration testing operations dashboard for managing projects, tasks, findings, clients, and assets with Next.js and MongoDB

Aradotso By Aradotso schedule Updated 6/7/2026

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);
  
Install via CLI
npx skills add https://github.com/Aradotso/security-skills --skill pentestops-dashboard
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator