docs: update README for group-based access control, security hardening, and current architecture

- Replace role-based docs with group-based (Admin, Standard_User, Leadership, Read_Only)
- Update API reference with correct group requirements and new endpoints (JIRA tickets, archive, todo-queue)
- Remove hardcoded default credentials from installation instructions
- Document SESSION_SECRET as required with generation instructions
- Add new migrations to install sequence (archive, timestamps, counts history, user_groups, created_by)
- Update architecture tree with new files (ivantiArchive, ComplianceChartsPanel, etc.)
- Update security model with rate limiting, sandbox iframe, rehype-sanitize, Content-Disposition sanitization
- Update database schema docs with created_by columns, user_group triggers, cascade deletes
- Fix middleware reference from requireRole to requireGroup
- Remove stale admin123 references throughout
This commit is contained in:
jramos
2026-04-07 11:29:33 -06:00
parent 8a6a3485e9
commit a711972054
2 changed files with 255 additions and 147 deletions

356
README.md
View File

@@ -13,7 +13,7 @@ A self-hosted vulnerability management dashboard for the NTS-AEO-STEAM and NTS-A
- [Configuration](#configuration)
- [Running the Application](#running-the-application)
- [Features](#features)
- [Authentication and User Roles](#authentication-and-user-roles)
- [Authentication and User Groups](#authentication-and-user-groups)
- [Home — CVE Management](#home--cve-management)
- [Reporting — Host Findings](#reporting--host-findings)
- [Ivanti Queue](#ivanti-queue)
@@ -46,7 +46,7 @@ The application provides:
- **AEO Compliance page** — weekly xlsx upload, diff preview, per-team metric health cards, device-level violation tracking with notes history
- Archer risk acceptance ticket tracking (EXC numbers) linked to CVE/vendor pairs
- A knowledge base for internal documentation and policies
- Role-based access control with a full audit trail
- Group-based access control (Admin, Standard_User, Leadership, Read_Only) with a full audit trail
---
@@ -54,11 +54,11 @@ The application provides:
| Layer | Technology |
|---|---|
| Backend | Node.js, Express 5 |
| Backend | Node.js 18+, Express 5 |
| Database | SQLite3 |
| File uploads | Multer 2 |
| Auth | bcryptjs, cookie-based sessions |
| Frontend | React 19, lucide-react, xlsx |
| Auth | bcryptjs, cookie-based sessions, express-rate-limit |
| Frontend | React 19, lucide-react, xlsx, rehype-sanitize |
| Compliance xlsx parsing | Python 3, pandas, openpyxl |
| Bulk notes import | Python 3 (stdlib only) |
@@ -84,7 +84,6 @@ cd cve-dashboard
### 2. Install backend dependencies
```bash
cd backend
npm install
```
@@ -107,7 +106,20 @@ apt install -y python3-pandas python3-openpyxl
> The bulk notes import script (`import_notes_from_csv.py`) uses only Python stdlib and does **not** require these packages.
### 5. Initialize the database
### 5. Configure environment variables
Create `backend/.env` — the server will refuse to start without `SESSION_SECRET`:
```bash
cd backend
cp .env.example .env
# Edit .env and set SESSION_SECRET to a random string:
# openssl rand -base64 32
```
See [Configuration](#configuration) for all available options.
### 6. Initialize the database
Run once from the `backend/` directory to create the SQLite database, all tables, indexes, and a default admin user:
@@ -116,13 +128,9 @@ cd backend
node setup.js
```
This creates `backend/cve_database.db` and a default admin account:
- Username: `admin`
- Password: `admin123`
This creates `backend/cve_database.db` and generates a random admin password printed to stdout. **Save the password — it is only shown once.**
**Change the admin password immediately after first login.**
### 6. Run database migrations
### 7. Run database migrations
Apply all feature migrations in order:
@@ -136,9 +144,14 @@ node migrations/add_ivanti_todo_queue_table.js
node migrations/add_card_workflow_type.js
node migrations/add_todo_queue_ip_address.js
node migrations/add_compliance_tables.js
node migrations/add_finding_archive_tables.js
node migrations/add_archer_tickets_timestamps.js
node migrations/add_ivanti_counts_history_table.js
node migrations/add_user_groups.js
node migrations/add_created_by_columns.js
```
### 7. Build the frontend
### 8. Build the frontend
```bash
cd frontend
@@ -159,7 +172,7 @@ The application is configured via `.env` files. These files are gitignored and m
PORT=3001
API_HOST=localhost
CORS_ORIGINS=http://YOUR_IP:3000
SESSION_SECRET=change-this-to-a-long-random-string
SESSION_SECRET=<generate with: openssl rand -base64 32>
NODE_ENV=production
# Optional: NVD API key for higher rate limits (50 req/30s vs 5 req/30s)
@@ -176,6 +189,8 @@ IVANTI_LAST_NAME=
IVANTI_SKIP_TLS=false
```
**`SESSION_SECRET` is required.** The server will exit on startup if it is not set. Generate one with `openssl rand -base64 32`.
### Frontend: `frontend/.env`
```env
@@ -225,17 +240,26 @@ npm start
## Features
### Authentication and User Roles
### Authentication and User Groups
All routes require authentication. Three roles are supported:
All routes require authentication. Four user groups are supported:
| Role | Permissions |
| Group | Permissions |
|---|---|
| `viewer` | Read-only: CVEs, documents, findings, reports, knowledge base, Archer tickets, compliance data |
| `editor` | All viewer permissions plus: create/update CVEs, upload documents, sync Ivanti findings, save notes and overrides, manage knowledge base, manage Archer tickets, upload compliance reports, manage Ivanti Queue |
| `admin` | All editor permissions plus: delete documents, delete reports, manage users, view audit logs |
| `Admin` | Full CRUD on all resources, user management, audit log access, export all data, delete any resource regardless of ownership |
| `Standard_User` | View all data, create and edit resources, delete own resources (with state and compliance restrictions), basic export (CSV/XLSX) |
| `Leadership` | View all data, export reports/compliance/visualizations, no create/edit/delete |
| `Read_Only` | View all data only — no create, edit, delete, or export |
Sessions expire after 24 hours. Session tokens are stored in `httpOnly` cookies.
**Standard User delete restrictions:**
- Can only delete resources they created (`created_by` ownership check)
- Cannot delete findings marked as resolved or closed
- Cannot delete tickets linked to compliance reports
- CVE deletion triggers a cascade impact check — if any associated Archer or JIRA ticket is compliance-linked, deletion is blocked and requires Admin intervention
Sessions expire after 24 hours. Session tokens are stored in `httpOnly` cookies. Login is rate-limited to 20 attempts per 15-minute window.
**Migration from legacy roles:** The `add_user_groups.js` migration automatically maps existing users: `admin``Admin`, `editor``Standard_User`, `viewer``Read_Only`. Unrecognized or NULL roles default to `Read_Only`.
---
@@ -249,11 +273,11 @@ The home page is the primary CVE research and tracking tool.
- Color-coded severity badges: Critical (red), High (amber), Medium (sky blue), Low (green)
- Paginated list view
**CVE Operations (editor/admin)**
**CVE Operations (Admin/Standard_User)**
- Add a new CVE entry — NVD auto-fill populates description, severity, and published date automatically
- Edit any field on an existing CVE entry
- Update status for all vendor rows matching a CVE ID in one click
- Delete a single vendor entry or all vendor entries for a CVE ID
- Delete a single vendor entry or all vendor entries for a CVE ID (ownership restrictions apply for Standard_User)
- The same CVE ID can be tracked across multiple vendors independently
**Document Management**
@@ -265,7 +289,7 @@ The home page is the primary CVE research and tracking tool.
**NVD Integration**
- Auto-fill CVE description, severity, and published date from the NIST NVD API 2.0 when adding a new CVE
- Bulk NVD Sync (editor/admin): fetch updated metadata for all CVEs in the database in one operation
- Bulk NVD Sync (Admin/Standard_User): fetch updated metadata for all CVEs in the database in one operation
- CVSS severity cascade: v3.1 preferred, then v3.0, then v2.0
- Rate-limit aware: respects NVD's 5 req/30s unauthenticated limit; with `NVD_API_KEY` the limit increases to 50 req/30s
@@ -285,7 +309,7 @@ The Reporting page is the core operational view for remediation tracking. It int
#### Syncing Data
Click **Sync** (top right) to pull the latest findings from Ivanti. The sync:
Click **Sync** (top right) to pull the latest findings from Ivanti. Sync requires Admin or Standard_User group. The sync:
1. Fetches all open host findings matching your BU filters and severity range (8.59.9 VRR)
2. Fetches the closed finding count separately
3. Sweeps closed findings to capture FP workflow states (including Approved FPs now closed)
@@ -324,19 +348,19 @@ Each row represents a single Ivanti host finding.
| Last Found | Last detection date from Ivanti |
| Notes | Free-form notes — inline editable, persists across syncs |
**Inline editing:** Click a Host or DNS cell to override the Ivanti value. An amber dot (●) marks overridden cells; use the revert button (↻) to restore the original. Overrides survive re-syncs.
**Inline editing:** Click a Host or DNS cell to override the Ivanti value. An amber dot (●) marks overridden cells; use the revert button (↻) to restore the original. Overrides survive re-syncs. Requires Admin or Standard_User group.
**Filtering:** Click ⊙ on any column header for multi-select filtering. The `— empty —` option filters to findings with no value in that column. Multiple filters are ANDed. The Action Coverage chart also acts as a filter.
**Column management:** Toggle visibility and drag to reorder via the **Columns** button. Order and visibility persist to `localStorage`.
**Export:** Click **Export** to download the current filtered view as CSV or XLSX.
**Export:** Click **Export** to download the current filtered view as CSV or XLSX. Requires Admin, Standard_User, or Leadership group.
---
### Ivanti Queue
A personal staging list for batch-processing FP, Archer, and CARD workflows without context-switching into Ivanti mid-review.
A personal staging list for batch-processing FP, Archer, and CARD workflows without context-switching into Ivanti mid-review. Requires Admin or Standard_User group.
**Adding items:** Check the checkbox at the far left of any finding row. A popover appears:
- For **FP** and **Archer** items: enter the Vendor / Platform (e.g., "Juniper MX", "Cisco IOS-XE")
@@ -364,7 +388,7 @@ The Compliance page tracks NTS-AEO team posture against the AEO compliance frame
#### Upload Workflow
Editors and admins can upload a new compliance report via the **Upload Report** button:
Admin and Standard_User groups can upload a new compliance report via the **Upload Report** button:
1. Drop or browse for the `NTS_AEO_YYYY_MM_DD.xlsx` file
2. The report is parsed server-side and a **diff preview** is shown — new violations, resolved items, and recurring items since the last upload
@@ -391,7 +415,7 @@ A slide-out panel for a selected device showing:
- For **2.3.x vulnerability metrics**: the `Ivanti_Vulnerability_ID` is displayed with a **View in Reporting →** button that navigates directly to the Reporting page
- **Resolved Metrics** — previously failing metrics now back in compliance
- **History** — how many times the device has appeared on the report and since when
- **Notes** — timestamped notes per metric with a multi-metric selector if multiple metrics are failing
- **Notes** — timestamped notes per metric with a multi-metric selector if multiple metrics are failing. Requires Admin or Standard_User group.
Notes persist across uploads and are keyed to the device hostname and metric ID.
@@ -405,11 +429,12 @@ Only **STEAM** and **ACCESS-ENG** teams are tracked. The team selector at the to
A document library for internal reference material — policies, runbooks, vendor advisories, and process guides.
- Upload documents with a title, optional description, and category
- View documents inline in the browser (PDFs render in an iframe; Markdown files render as HTML)
- Upload documents with a title, optional description, and category (Admin/Standard_User)
- View documents inline in the browser (PDFs render in a sandboxed iframe; Markdown files render as sanitized HTML)
- Download any document
- Filter and browse by category
- Editors and admins can upload and delete; all authenticated users can view
- Admin can delete any article; Standard_User can delete articles they created
- All authenticated users can view
Allowed file types: PDF, Markdown, TXT, Office documents (DOC, DOCX, XLS, XLSX, PPT, PPTX), HTML, JSON, YAML, and images (PNG, JPG, GIF).
@@ -417,7 +442,7 @@ Allowed file types: PDF, Markdown, TXT, Office documents (DOC, DOCX, XLS, XLSX,
### Exports
Bulk export tools for reports and data extracts.
Bulk export tools for reports and data extracts. Available to Admin, Standard_User, and Leadership groups. Read_Only users cannot access the Exports page.
---
@@ -430,15 +455,18 @@ Track Archer exception tickets (EXC numbers) linked to specific CVE/vendor pairs
- Optional Archer URL field for deep-linking to the Archer record
- Filter tickets by CVE ID, vendor, or status
- Clicking an EXC badge on the Home page navigates to the Reporting page pre-filtered to findings with that EXC number in their notes
- Admin/Standard_User can create, edit, and delete tickets (Standard_User delete subject to ownership and compliance linkage checks)
---
### User Management (Admin)
- Create users with a role assignment
- Change username, email, password, role, or active status
- Create users with a group assignment (Admin, Standard_User, Leadership, Read_Only)
- Change username, email, password, group, or active status
- Group changes require confirmation; downgrading an Admin shows an additional warning
- Deactivating a user immediately invalidates all their active sessions
- Admins cannot demote themselves or deactivate their own account
- All group changes are audit-logged with previous and new group values
---
@@ -506,130 +534,147 @@ python3 import_notes_from_csv.py input.csv --db /path/to/cve_database.db
## API Reference
All endpoints are prefixed with `/api`. All endpoints except `/api/auth/login` and `/api/auth/logout` require a valid session cookie.
All endpoints are prefixed with `/api`. All endpoints except `/api/auth/login` and `/api/auth/logout` require a valid session cookie. Group requirements are listed per endpoint.
### Auth
| Method | Path | Auth | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| POST | `/api/auth/login` | Public | Log in, receive session cookie |
| POST | `/api/auth/login` | Public | Log in, receive session cookie (rate-limited: 20/15min) |
| POST | `/api/auth/logout` | Public | Invalidate session |
| GET | `/api/auth/me` | Session | Get current user info |
| POST | `/api/auth/cleanup-sessions` | Session | Delete expired sessions |
| GET | `/api/auth/me` | Any | Get current user info (returns `group` field) |
| POST | `/api/auth/cleanup-sessions` | Admin | Delete expired sessions |
### CVEs
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/cves` | viewer+ | List CVEs; query params: `search`, `vendor`, `severity`, `status` |
| POST | `/api/cves` | editor+ | Create a new CVE entry |
| PUT | `/api/cves/:id` | editor+ | Update a CVE entry by row ID |
| PATCH | `/api/cves/:cveId/status` | editor+ | Update status for all vendor rows matching a CVE ID |
| DELETE | `/api/cves/:id` | editor+ | Delete a single CVE vendor entry |
| DELETE | `/api/cves/by-cve-id/:cveId` | editor+ | Delete all vendor entries for a CVE ID |
| GET | `/api/cves/check/:cveId` | viewer+ | Quick check: existence and status of a CVE |
| GET | `/api/cves/distinct-ids` | viewer+ | All distinct CVE IDs (used by NVD sync) |
| GET | `/api/cves/:cveId/vendors` | viewer+ | All vendor entries for a specific CVE ID |
| GET | `/api/cves` | Any | List CVEs; query params: `search`, `vendor`, `severity`, `status` |
| POST | `/api/cves` | Admin, Standard_User | Create a new CVE entry |
| PUT | `/api/cves/:id` | Admin, Standard_User | Update a CVE entry by row ID |
| PATCH | `/api/cves/:cveId/status` | Admin, Standard_User | Update status for all vendor rows matching a CVE ID |
| DELETE | `/api/cves/:id` | Admin, Standard_User | Delete a single CVE vendor entry (ownership + cascade check for Standard_User) |
| DELETE | `/api/cves/by-cve-id/:cveId` | Admin, Standard_User | Delete all vendor entries for a CVE ID (ownership + cascade check for Standard_User) |
| GET | `/api/cves/check/:cveId` | Any | Quick check: existence and status of a CVE |
| GET | `/api/cves/distinct-ids` | Any | All distinct CVE IDs (used by NVD sync) |
| GET | `/api/cves/:cveId/vendors` | Any | All vendor entries for a specific CVE ID |
| GET | `/api/cves/compliance` | Any | Document compliance status view |
### Documents
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/cves/:cveId/documents` | viewer+ | List documents for a CVE; optional `?vendor=` filter |
| POST | `/api/cves/:cveId/documents` | editor+ | Upload a document for a CVE/vendor pair |
| DELETE | `/api/documents/:id` | admin | Delete a document and its file from disk |
| GET | `/api/cves/:cveId/documents` | Any | List documents for a CVE; optional `?vendor=` filter |
| POST | `/api/cves/:cveId/documents` | Admin, Standard_User | Upload a document for a CVE/vendor pair |
| DELETE | `/api/documents/:id` | Admin | Delete a document and its file from disk |
### NVD
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/nvd/lookup/:cveId` | viewer+ | Look up a single CVE in the NVD 2.0 API |
| POST | `/api/cves/nvd-sync` | editor+ | Bulk update CVE metadata from NVD |
| GET | `/api/nvd/lookup/:cveId` | Any | Look up a single CVE in the NVD 2.0 API |
| POST | `/api/cves/nvd-sync` | Admin, Standard_User | Bulk update CVE metadata from NVD |
### JIRA Tickets
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/jira-tickets` | Any | List tickets; optional filters: `cve_id`, `vendor`, `status` |
| POST | `/api/jira-tickets` | Admin, Standard_User | Create a JIRA ticket |
| PUT | `/api/jira-tickets/:id` | Admin, Standard_User | Update a JIRA ticket |
| DELETE | `/api/jira-tickets/:id` | Admin, Standard_User | Delete a JIRA ticket (ownership + compliance check for Standard_User) |
### Ivanti — Host Findings
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/ivanti/findings` | viewer+ | Get cached findings with notes and overrides merged in |
| POST | `/api/ivanti/findings/sync` | viewer+ | Trigger an immediate findings sync from Ivanti |
| GET | `/api/ivanti/findings/counts` | viewer+ | Open vs closed finding totals |
| GET | `/api/ivanti/findings/fp-workflow-counts` | viewer+ | FP workflow state breakdown |
| PUT | `/api/ivanti/findings/:findingId/override` | editor+ | Override `hostName` or `dns`; empty value clears the override |
| PUT | `/api/ivanti/findings/:findingId/note` | viewer+ | Save or update a finding note (max 255 chars) |
| GET | `/api/ivanti/findings` | Any | Get cached findings with notes and overrides merged in |
| POST | `/api/ivanti/findings/sync` | Admin, Standard_User | Trigger an immediate findings sync from Ivanti |
| GET | `/api/ivanti/findings/counts` | Any | Open vs closed finding totals |
| GET | `/api/ivanti/findings/fp-workflow-counts` | Any | FP workflow state breakdown |
| PUT | `/api/ivanti/findings/:findingId/override` | Admin, Standard_User | Override `hostName` or `dns`; empty value clears the override |
| PUT | `/api/ivanti/findings/:findingId/note` | Admin, Standard_User | Save or update a finding note (max 255 chars) |
### Ivanti — Workflows
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/ivanti/workflows` | viewer+ | Get cached workflow data |
| POST | `/api/ivanti/workflows/sync` | viewer+ | Trigger an immediate workflow sync |
| GET | `/api/ivanti/workflows` | Any | Get cached workflow data |
| POST | `/api/ivanti/workflows/sync` | Admin, Standard_User | Trigger an immediate workflow sync |
### Ivanti Queue
### Ivanti — Todo Queue
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/ivanti/queue` | viewer+ | Get all queue items for the current user |
| POST | `/api/ivanti/queue` | editor+ | Add a finding to the queue |
| PATCH | `/api/ivanti/queue/:id` | editor+ | Update a queue item (mark complete, edit vendor/type) |
| DELETE | `/api/ivanti/queue/:id` | editor+ | Delete a single queue item |
| DELETE | `/api/ivanti/queue` | editor+ | Delete multiple queue items (body: `{ ids: [...] }`) |
| GET | `/api/ivanti/todo-queue` | Any | Get all queue items for the current user |
| POST | `/api/ivanti/todo-queue` | Admin, Standard_User | Add a finding to the queue |
| PUT | `/api/ivanti/todo-queue/:id` | Admin, Standard_User | Update a queue item (mark complete, edit vendor/type) |
| DELETE | `/api/ivanti/todo-queue/:id` | Admin, Standard_User | Delete a single queue item |
| DELETE | `/api/ivanti/todo-queue/completed` | Admin, Standard_User | Delete all completed queue items |
### Ivanti — Archive
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/ivanti/archive` | Any | Get finding archive data for severity score drift tracking |
### Compliance
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| POST | `/api/compliance/preview` | editor+ | Parse an xlsx upload and return diff + temp file path |
| POST | `/api/compliance/commit` | editor+ | Commit a previewed upload to the database |
| GET | `/api/compliance/uploads` | viewer+ | List all compliance upload records |
| GET | `/api/compliance/summary` | viewer+ | Metric health summary; `?team=STEAM` |
| GET | `/api/compliance/items` | viewer+ | Device list; `?team=STEAM&status=active` |
| GET | `/api/compliance/items/:hostname` | viewer+ | Full detail for a device (metrics + notes) |
| GET | `/api/compliance/notes/:hostname/:metricId` | viewer+ | Notes for a specific hostname/metric |
| POST | `/api/compliance/notes` | editor+ | Add a note for a hostname/metric |
| POST | `/api/compliance/preview` | Admin, Standard_User | Parse an xlsx upload and return diff + temp file path |
| POST | `/api/compliance/commit` | Admin, Standard_User | Commit a previewed upload to the database |
| GET | `/api/compliance/uploads` | Any | List all compliance upload records |
| GET | `/api/compliance/summary` | Any | Metric health summary; `?team=STEAM` |
| GET | `/api/compliance/items` | Any | Device list; `?team=STEAM&status=active` |
| GET | `/api/compliance/items/:hostname` | Any | Full detail for a device (metrics + notes) |
| GET | `/api/compliance/notes/:hostname/:metricId` | Any | Notes for a specific hostname/metric |
| POST | `/api/compliance/notes` | Admin, Standard_User | Add a note for a hostname/metric |
### Knowledge Base
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| POST | `/api/knowledge-base/upload` | editor+ | Upload a new knowledge base document |
| GET | `/api/knowledge-base` | viewer+ | List all articles |
| GET | `/api/knowledge-base/:id` | viewer+ | Get article metadata |
| GET | `/api/knowledge-base/:id/content` | viewer+ | Get file content for inline display |
| GET | `/api/knowledge-base/:id/download` | viewer+ | Download the file |
| DELETE | `/api/knowledge-base/:id` | editor+ | Delete article and file |
| POST | `/api/knowledge-base/upload` | Admin, Standard_User | Upload a new knowledge base document |
| GET | `/api/knowledge-base` | Any | List all articles |
| GET | `/api/knowledge-base/:id` | Any | Get article metadata |
| GET | `/api/knowledge-base/:id/content` | Any | Get file content for inline display |
| GET | `/api/knowledge-base/:id/download` | Any | Download the file |
| DELETE | `/api/knowledge-base/:id` | Admin, Standard_User | Delete article and file (Standard_User: own articles only) |
### Archer Tickets
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/archer-tickets` | viewer+ | List tickets; optional filters: `cve_id`, `vendor`, `status` |
| POST | `/api/archer-tickets` | editor+ | Create a new Archer ticket |
| PUT | `/api/archer-tickets/:id` | editor+ | Update an Archer ticket |
| DELETE | `/api/archer-tickets/:id` | editor+ | Delete an Archer ticket |
| GET | `/api/archer-tickets` | Any | List tickets; optional filters: `cve_id`, `vendor`, `status` |
| GET | `/api/archer-tickets/status-trend` | Any | Ticket counts by date and status for pipeline chart |
| POST | `/api/archer-tickets` | Admin, Standard_User | Create a new Archer ticket |
| PUT | `/api/archer-tickets/:id` | Admin, Standard_User | Update an Archer ticket |
| DELETE | `/api/archer-tickets/:id` | Admin, Standard_User | Delete an Archer ticket (ownership + compliance check for Standard_User) |
### Users (Admin only)
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/users` | admin | List all users |
| GET | `/api/users/:id` | admin | Get a single user |
| POST | `/api/users` | admin | Create a user |
| PATCH | `/api/users/:id` | admin | Update a user |
| DELETE | `/api/users/:id` | admin | Delete a user |
| GET | `/api/users` | Admin | List all users |
| GET | `/api/users/:id` | Admin | Get a single user |
| POST | `/api/users` | Admin | Create a user |
| PATCH | `/api/users/:id` | Admin | Update a user |
| DELETE | `/api/users/:id` | Admin | Delete a user |
### Audit Logs (Admin only)
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/audit-logs` | admin | Paginated audit log; filters: `user`, `action`, `entityType`, `startDate`, `endDate` |
| GET | `/api/audit-logs/actions` | admin | List distinct action types for filter dropdowns |
| GET | `/api/audit-logs` | Admin | Paginated audit log; filters: `user`, `action`, `entityType`, `startDate`, `endDate` |
| GET | `/api/audit-logs/actions` | Admin | List distinct action types for filter dropdowns |
### Utility
| Method | Path | Role | Description |
| Method | Path | Group | Description |
|---|---|---|---|
| GET | `/api/vendors` | viewer+ | List all distinct vendor names |
| GET | `/api/stats` | viewer+ | Dashboard statistics |
| GET | `/api/vendors` | Any | List all distinct vendor names |
| GET | `/api/stats` | Any | Dashboard statistics |
---
@@ -639,6 +684,7 @@ All endpoints are prefixed with `/api`. All endpoints except `/api/auth/login` a
cve-dashboard/
├── start-servers.sh # Start backend + frontend in background
├── stop-servers.sh # Stop all servers
├── package.json # Root package.json (backend dependencies)
├── backend/
│ ├── server.js # Express app — routes, middleware, security headers
@@ -649,7 +695,7 @@ cve-dashboard/
│ │ ├── knowledge_base/ # Knowledge base documents
│ │ └── temp/ # Temporary upload staging
│ ├── routes/
│ │ ├── auth.js # Login, logout, session check
│ │ ├── auth.js # Login, logout, session check, rate limiting
│ │ ├── users.js # User CRUD (admin)
│ │ ├── auditLog.js # Audit log viewer (admin)
│ │ ├── nvdLookup.js # NVD API proxy
@@ -658,20 +704,13 @@ cve-dashboard/
│ │ ├── ivantiWorkflows.js # Ivanti workflow batch sync and cache
│ │ ├── ivantiFindings.js # Ivanti host findings sync, notes, overrides, FP counts
│ │ ├── ivantiTodoQueue.js # Ivanti Queue — personal FP/Archer/CARD staging list
│ │ ├── ivantiArchive.js # Finding archive for severity score drift
│ │ └── compliance.js # AEO compliance upload, diff, device tracking, notes
│ ├── middleware/
│ │ └── auth.js # requireAuth and requireRole middleware
│ │ └── auth.js # requireAuth and requireGroup middleware
│ ├── helpers/
│ │ ── auditLog.js # logAudit helper (fire-and-forget)
│ ├── migrations/
│ │ ├── add_knowledge_base_table.js
│ │ ├── add_archer_tickets_table.js
│ │ ├── add_ivanti_sync_table.js
│ │ ├── add_ivanti_findings_tables.js
│ │ ├── add_ivanti_todo_queue_table.js # Ivanti Queue table
│ │ ├── add_card_workflow_type.js # CARD workflow type support
│ │ ├── add_todo_queue_ip_address.js # IP address column on queue items
│ │ └── add_compliance_tables.js # AEO compliance tables
│ │ ── auditLog.js # logAudit helper (fire-and-forget)
│ ├── migrations/ # Sequential migration scripts (run manually with node)
│ └── scripts/
│ ├── parse_compliance_xlsx.py # Parses NTS_AEO xlsx compliance reports
│ ├── import_notes_from_csv.py # Bulk-import finding notes from CSV
@@ -682,24 +721,27 @@ cve-dashboard/
├── App.js # Home dashboard — CVE list, filters, modals, calendar
├── App.css # Global styles and CSS variables
├── contexts/
│ └── AuthContext.js # Auth state provider (login, logout, role helpers)
│ └── AuthContext.js # Auth state provider (login, logout, group helpers)
└── components/
├── LoginForm.js # Login page
├── NavDrawer.js # Side navigation drawer
├── UserMenu.js # User dropdown in header
├── NavDrawer.js # Side navigation drawer (Admin Panel link for Admin group)
├── UserMenu.js # User dropdown in header (shows group badge)
├── CalendarWidget.js # Due-date calendar with Ivanti finding indicators
├── UserManagement.js # Admin user management panel
├── UserManagement.js # Admin user management panel (group assignment)
├── AuditLog.js # Admin audit log viewer
├── NvdSyncModal.js # Bulk NVD sync dialog
├── KnowledgeBaseModal.js # Knowledge base upload/list modal
├── KnowledgeBaseViewer.js # Inline document viewer
├── KnowledgeBaseViewer.js # Inline document viewer (sandboxed iframe, sanitized markdown)
└── pages/
├── ReportingPage.js # Host findings: charts, table, queue, export
├── CompliancePage.js # AEO compliance: metric cards, device table
├── ComplianceUploadModal.js # xlsx upload with diff preview
├── ComplianceDetailPanel.js # Per-device metrics, history, notes
├── ComplianceChartsPanel.js # Compliance trend charts
├── IvantiCountsChart.js # Ivanti counts history chart
├── ArchiveSummaryBar.js # Finding archive summary
├── KnowledgeBasePage.js # Knowledge base page
└── ExportsPage.js # Exports page
└── ExportsPage.js # Exports page (group-gated)
```
---
@@ -708,13 +750,13 @@ cve-dashboard/
### Core tables (created by `setup.js`)
**`cves`** — One row per CVE/vendor pair. `UNIQUE(cve_id, vendor)`.
**`cves`** — One row per CVE/vendor pair. `UNIQUE(cve_id, vendor)`. Includes `created_by` column for ownership tracking.
**`documents`** — Files attached to a CVE/vendor pair. Foreign key to `cves(cve_id)`.
**`documents`** — Files attached to a CVE/vendor pair. Foreign key to `cves(cve_id)` with `ON DELETE CASCADE`.
**`required_documents`** — Vendor-specific document requirements.
**`users`** — Accounts with roles: `admin`, `editor`, `viewer`.
**`users`** — Accounts with group-based access control. `user_group` column with values: `Admin`, `Standard_User`, `Leadership`, `Read_Only`. Enforced by INSERT/UPDATE triggers. Legacy `role` column retained for rollback safety.
**`sessions`** — Active sessions with 24-hour expiry.
@@ -722,9 +764,11 @@ cve-dashboard/
### Feature tables (added by migrations)
**`knowledge_base`** — Document library entries with title, slug, category, description, and file metadata.
**`knowledge_base`** — Document library entries with title, slug, category, description, file metadata, and `created_by`.
**`archer_tickets`** — Archer EXC exception tickets linked to CVE/vendor pairs. `UNIQUE(exc_number)`.
**`archer_tickets`** — Archer EXC exception tickets linked to CVE/vendor pairs. `UNIQUE(exc_number)`. Includes `created_by` for ownership tracking. Foreign key to `cves(cve_id, vendor)` with `ON DELETE CASCADE`.
**`jira_tickets`** — JIRA tickets linked to CVE/vendor pairs. Includes `created_by`. Foreign key to `cves(cve_id, vendor)` with `ON DELETE CASCADE`.
**`ivanti_sync_state`** — Single-row cache for Ivanti workflow batch data.
@@ -752,18 +796,47 @@ cve-dashboard/
## Security Model
### Authentication
- Cookie-based sessions with `httpOnly: true`, `sameSite: lax`, `secure: true` (in production)
- Sessions expire after 24 hours
- Login rate-limited to 20 attempts per 15-minute window via `express-rate-limit`
- `SESSION_SECRET` is required — server refuses to start without it
### Group-based access control
Four groups with distinct permission boundaries enforced server-side via `requireGroup` middleware:
| Capability | Admin | Standard_User | Leadership | Read_Only |
|---|---|---|---|---|
| View all data | ✓ | ✓ | ✓ | ✓ |
| Create/edit resources | ✓ | ✓ | ✗ | ✗ |
| Delete own resources | ✓ | ✓ (restricted) | ✗ | ✗ |
| Delete any resource | ✓ | ✗ | ✗ | ✗ |
| Export (CSV/XLSX) | ✓ | ✓ | ✓ | ✗ |
| Admin panel / user management | ✓ | ✗ | ✗ | ✗ |
Standard_User delete restrictions are enforced at the API level: ownership check, finding state check, compliance linkage check, and cascade impact check for CVEs.
### File upload security
- Extension allowlist enforced by Multer; executables (`.exe`, `.js`, `.sh`, `.py`, `.bat`, etc.) are blocked
- MIME type prefix validation in addition to extension checking
- 10 MB per-file size limit
- Filenames are sanitized: path separators, `..` sequences, null bytes, and non-alphanumeric characters are removed
- Content-Disposition headers sanitize filenames to prevent header injection
### Path traversal prevention
- `sanitizePathSegment()` strips `/`, `\`, `..`, and null bytes from any value used in `path.join()`
- `isPathWithinUploads()` verifies resolved paths stay within the uploads root before any file operation
### Content security
- Knowledge base PDF iframe uses `sandbox="allow-same-origin"` to prevent script execution
- Markdown rendering uses `rehype-sanitize` to strip dangerous HTML
- CSP `frame-ancestors` header derived from `CORS_ORIGINS` environment variable
### Input validation
- CVE ID must match `/^CVE-\d{4}-\d{4,}$/`
@@ -771,14 +844,10 @@ cve-dashboard/
- Status must be one of: `Open`, `Addressed`, `In Progress`, `Resolved`
- Archer EXC numbers must match `/^EXC-\d+$/`
- Finding override field must be one of: `hostName`, `dns`
- User group validated against: `Admin`, `Standard_User`, `Leadership`, `Read_Only` (enforced by DB triggers and app-level validation)
- Hostname format validated with `/^[a-zA-Z0-9._-]+$/` in compliance notes
- All database operations use prepared statements — no string interpolation in SQL
### Error handling
- 500 responses never expose internal error messages to the client
- Full errors are logged server-side only
- Descriptive 400/409 responses contain only application-authored validation messages
### Security headers
Applied to all responses:
@@ -789,15 +858,11 @@ Applied to all responses:
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`
### Session cookies
`httpOnly: true`, `sameSite: lax`, `secure: true` in production (`NODE_ENV=production`).
---
## Migrations
Migrations are standalone Node.js scripts. Run them in the listed order on a fresh install. All use `CREATE TABLE IF NOT EXISTS` or `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` and are safe to re-run.
Migrations are standalone Node.js scripts. Run them in the listed order on a fresh install. All are idempotent and safe to re-run.
```bash
cd backend
@@ -809,6 +874,11 @@ node migrations/add_ivanti_todo_queue_table.js
node migrations/add_card_workflow_type.js
node migrations/add_todo_queue_ip_address.js
node migrations/add_compliance_tables.js
node migrations/add_finding_archive_tables.js
node migrations/add_archer_tickets_timestamps.js
node migrations/add_ivanti_counts_history_table.js
node migrations/add_user_groups.js
node migrations/add_created_by_columns.js
```
For deployments upgrading from an older schema, the following legacy migration scripts are also available in `backend/`:

View File

@@ -16,7 +16,20 @@ const loginLimiter = rateLimit({
function createAuthRouter(db, logAudit) {
const router = express.Router();
// Login
/**
* POST /api/auth/login
*
* Authenticates a user with username and password, creates a session,
* and sets an httpOnly session cookie. Rate-limited to 20 attempts per 15 minutes.
*
* @body {string} username - The user's login username
* @body {string} password - The user's password
* @returns {object} 200 - { message: 'Login successful', user: { id, username, email, group } }
* @returns {object} 400 - { error: 'Username and password are required' }
* @returns {object} 401 - { error: 'Invalid username or password' } | { error: 'Account is disabled' }
* @returns {object} 429 - { error: 'Too many login attempts. Please try again in 15 minutes.' }
* @returns {object} 500 - { error: 'Login failed' }
*/
router.post('/login', loginLimiter, async (req, res) => {
const { username, password } = req.body;
@@ -139,7 +152,14 @@ function createAuthRouter(db, logAudit) {
}
});
// Logout
/**
* POST /api/auth/logout
*
* Ends the current user session by deleting it from the database
* and clearing the session cookie.
*
* @returns {object} 200 - { message: 'Logged out successfully' }
*/
router.post('/logout', async (req, res) => {
const sessionId = req.cookies?.session_id;
@@ -182,7 +202,16 @@ function createAuthRouter(db, logAudit) {
res.json({ message: 'Logged out successfully' });
});
// Get current user
/**
* GET /api/auth/me
*
* Returns the currently authenticated user based on the session cookie.
* Clears the cookie and returns 401 if the session is expired or the account is disabled.
*
* @returns {object} 200 - { user: { id, username, email, group } }
* @returns {object} 401 - { error: 'Not authenticated' } | { error: 'Session expired' } | { error: 'Account is disabled' }
* @returns {object} 500 - { error: 'Failed to get user' }
*/
router.get('/me', async (req, res) => {
const sessionId = req.cookies?.session_id;
@@ -229,7 +258,16 @@ function createAuthRouter(db, logAudit) {
}
});
// Clean up expired sessions (admin only)
/**
* POST /api/auth/cleanup-sessions
*
* Deletes all expired sessions from the database. Requires Admin group.
*
* @returns {object} 200 - { message: 'Expired sessions cleaned up' }
* @returns {object} 401 - { error: 'Authentication required' }
* @returns {object} 403 - { error: 'Insufficient permissions', required: ['Admin'], current: '...' }
* @returns {object} 500 - { error: 'Cleanup failed' }
*/
router.post('/cleanup-sessions', requireAuth(db), requireGroup('Admin'), async (req, res) => {
try {
await new Promise((resolve, reject) => {