A full-stack vulnerability management application for penetration testers to manage clients, projects, findings, and generate reviewable assessment reports with a hard no-review → no-export gate and true WYSIWYG PDF export.
| Layer | Technology |
|---|---|
| Backend | FastAPI · SQLAlchemy 2.0 · Pydantic v2 |
| Database | SQLite (local dev) — PostgreSQL-ready via DATABASE_URL |
| Auth | JWT (PyJWT) + bcrypt, role-aware (Tester / Reviewer) |
| Playwright (Chromium) — renders the same HTML as the browser preview | |
| Frontend | React + TypeScript · Vite · Tailwind CSS · React Router |
| Charts | Recharts (dashboard) + server-rendered SVG (report) |
The backend renders one canonical report HTML from a Jinja2 template
(backend/app/templates/report.html). The frontend preview embeds that exact HTML
in an iframe, and PDF export runs Playwright/Chromium over the same HTML. Same bytes
through the same rendering engine → the preview and the PDF are identical. Evidence images
are inlined as base64 data URIs and the severity chart is server-rendered SVG, so the report
is fully self-contained.
- Python 3.11+ (developed on 3.13)
- Node.js 18+ (developed on 22)
cd backend
# 1. Create a virtual environment
python -m venv .venv
# Windows PowerShell: .venv\Scripts\Activate.ps1
# macOS/Linux: source .venv/bin/activate
# 2. Install dependencies
pip install -r requirements.txt
# 3. Install the Chromium browser used for PDF export (one-time, ~download)
python -m playwright install chromium
# 4. (optional) configure environment
cp .env.example .env # defaults work out of the box for local dev
# 5. Seed demo data (users + a sample engagement with two versions)
python seed.py
# 6. Run the API -> http://localhost:8000 (docs at /docs)
python -m uvicorn app.main:app --reload --port 8000If you skip
playwright install chromium, the app still runs and previews work; only PDF export returns a 503 with a clear message until Chromium is installed.
cd frontend
npm install
npm run dev # -> http://localhost:5173The frontend talks to http://localhost:8000 by default. To point elsewhere, set
VITE_API_URL in frontend/.env.
| Role | Password | |
|---|---|---|
| Tester | tester@pthub.io |
Passw0rd! |
| Reviewer | reviewer@pthub.io |
Passw0rd! |
The seed creates client Acme Financial → project Acme Online Banking with:
- v1.0 — Approved (export unlocked, PDF downloadable)
- v2.0 — Pending Review (export locked) and a meaningful diff vs v1.0 (1 New, 1 Regressed, 2 Still Open, 2 Resolved).
- A Tester adds findings to a project version and clicks Mark Ready for Review. The version enters Pending Review and export buttons lock.
- A Reviewer opens the version, adds per-finding comments, writes an overall verdict, and clicks Approve or Request Changes.
- Only after Approved does the report unlock for export.
The gate is enforced in both places:
- Frontend — the Export button is disabled and visibly locked (🔒) unless
can_export. - Backend —
GET /versions/{id}/report/export.pdfreturns 403 unless the version isApproved.
Safety properties enforced server-side:
- A reviewer cannot approve a version they authored (
author_id != reviewer_id). - Roles are distinct: only Testers create/edit data; only Reviewers post review comments and verdicts.
- Dashboard — client/project/version counts, open findings by severity (Recharts), reports pending review.
- Clients → Client detail (their projects).
- Project detail — version timeline + version-diff view (New / Resolved / Still Open / Regressed).
- Version detail — findings table (filter/sort by severity & status) + review panel.
- Finding editor — standardized layout (Severity beside title, Category, Description, Implications, Remediation, References) with URL validation, CVSS/CWE/OWASP, evidence text + image upload, and the per-finding review-comment thread.
- Report builder — editable metadata tables (Document Approval, Version Control, Review) then the review gate.
- Report preview — WYSIWYG iframe with the export button locked until approved.
PT-Hub/
├─ backend/
│ ├─ app/
│ │ ├─ main.py # FastAPI app, CORS, router mounts, static uploads
│ │ ├─ config.py # pydantic-settings (env)
│ │ ├─ database.py # engine + session
│ │ ├─ models.py # SQLAlchemy models
│ │ ├─ schemas.py # Pydantic schemas (URL + CVSS validation)
│ │ ├─ enums.py # shared enums
│ │ ├─ security.py # bcrypt + JWT
│ │ ├─ deps.py # auth dependencies / role guards
│ │ ├─ serializers.py # ORM -> response (computed fields)
│ │ ├─ api/ # routers: auth, clients, projects, versions,
│ │ │ # findings, reports, dashboard
│ │ ├─ services/ # diff engine, report_render (Jinja2+SVG), pdf (Playwright)
│ │ └─ templates/report.html
│ ├─ seed.py
│ └─ requirements.txt
└─ frontend/
└─ src/
├─ api/ # client.ts, index.ts (typed), types.ts (mirror schemas)
├─ auth/AuthContext.tsx
├─ components/ # badges, layout, modal, finding editor
├─ lib/ # severity palette + constants
└─ pages/ # Login, Dashboard, Clients, ClientDetail,
# ProjectDetail, VersionDetail, ReportBuilder, ReportPreview
All routes are under /api. Highlights:
| Method | Path | Notes |
|---|---|---|
POST |
/auth/login |
returns JWT + user |
GET |
/dashboard |
aggregate stats |
GET/POST |
/clients, /projects, … |
CRUD (writes require Tester) |
POST |
/projects/{id}/versions |
optional clone_from_version_id for re-tests |
GET |
/versions/{id}/diff |
diff vs previous (or ?base_version_id=) |
POST |
/versions/{id}/ready-for-review |
Tester submits |
POST |
/versions/{id}/review |
Reviewer Approve / Request Changes |
POST |
/findings/{id}/comments |
per-finding review comment (Reviewer) |
POST |
/findings/{id}/evidence |
evidence image upload |
GET |
/versions/{id}/report/preview |
canonical HTML (iframe) |
GET |
/versions/{id}/report/export.pdf |
403 unless Approved |
Interactive docs: http://localhost:8000/docs
- Database: set
DATABASE_URLto a PostgreSQL URL (postgresql+psycopg://…) and addpsycopg[binary]to use Postgres. Tables are auto-created on startup for dev; use Alembic migrations for production. - Secrets: change
SECRET_KEYoutside local dev. - Report content (descriptions, evidence) is auto-escaped in the Jinja2 template to prevent injection into the rendered report/PDF.