From 7f7d3a2977a5def6c967c006e9d07acae65ea5b0 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Fri, 1 May 2026 21:11:47 +0000 Subject: [PATCH] =?UTF-8?q?release:=20v1.0.0=20=E2=80=94=20clean=20README,?= =?UTF-8?q?=20changelog,=20full=20reference=20manual,=20dead=20code=20remo?= =?UTF-8?q?val,=20package=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 59 ++ README.md | 1149 ++------------------------ backend/.env.example | 4 + backend/migrate-audit-log.js | 96 --- backend/migrate-to-1.1.js | 289 ------- backend/migrate_jira_tickets.js | 39 - backend/migrate_multivendor.js | 128 --- docs/guides/full-reference-manual.md | 1080 ++++++++++++++++++++++++ frontend/src/App.js.v1 | 598 -------------- frontend/src/Default-App.js | 25 - package.json | 6 +- 11 files changed, 1234 insertions(+), 2239 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 backend/migrate-audit-log.js delete mode 100755 backend/migrate-to-1.1.js delete mode 100644 backend/migrate_jira_tickets.js delete mode 100644 backend/migrate_multivendor.js create mode 100644 docs/guides/full-reference-manual.md delete mode 100644 frontend/src/App.js.v1 delete mode 100644 frontend/src/Default-App.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..18fe9cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,59 @@ +# Changelog + +## v1.0.0 — 2026-05-01 + +First official release. Consolidates all features developed since initial commit into a stable, documented, deployment-ready package. + +### Core Platform +- CVE tracking with multi-vendor support, document storage, and NVD API auto-fill +- Session-based authentication with four user groups (Admin, Standard_User, Leadership, Read_Only) +- Full audit logging of all state-changing actions +- Dark tactical intelligence UI theme with monospace typography + +### Ivanti Integration +- Live sync of open host findings from Ivanti/RiskSense API (auto-sync every 24h) +- Reporting page with donut metric charts, advanced per-column filtering, inline editing +- FP workflow submission directly to Ivanti API with file attachments +- Ivanti Queue — personal staging list for batch FP, Archer, CARD, and Granite workflows +- Queue item redirect between workflow types after completion +- Row visibility controls with localStorage persistence + +### Archive and Anomaly Tracking +- Automatic detection of disappeared and returned findings across syncs +- BU drift checker — classifies archived findings by reason (BU reassignment, severity drift, closed on platform, decommissioned) +- Return classification — explains why findings came back (BU reassigned back, severity re-escalated, etc.) +- Findings Trend chart with archive activity sparkline and shift reason tooltips +- Anomaly banner for significant archive events + +### Compliance (AEO Posture) +- Weekly NTS_AEO xlsx upload with diff preview (new, resolved, recurring) +- Schema drift detection with breaking/silent-miss/cosmetic classification +- Admin config reconciliation for parser updates +- Per-team metric health cards with grouped categories and variant pills +- Device-level violation tracking with timestamped notes history +- Multi-metric note grouping +- Upload rollback support + +### Integrations +- Jira Data Center — create, sync, and track tickets linked to CVE/vendor pairs +- Archer — risk acceptance exception tracking (EXC numbers) +- Atlas InfoSec — action plan cache, bulk creation from row selection, metrics reporting +- CARD API — Granite/CARD asset lookup for network device workflows +- NVD API — auto-fill CVE metadata with bulk sync support + +### Knowledge Base +- Internal document library with inline PDF and Markdown rendering +- Category-based browsing and search + +### Admin +- Full-page admin panel with user management, audit log, and system info tabs +- Themed confirm modals replacing browser dialogs +- User profile panel with self-service password change + +### Infrastructure +- Consolidated `setup.js` with complete database schema (27 tables, all indexes and triggers) +- systemd service files for persistent deployment +- GitLab CI/CD pipeline (install, lint, test, build, deploy) +- GPG-signed commits for code provenance +- Organized documentation structure (api, design, guides, security, testing, troubleshooting) +- Migration scripts documented and retained for existing deployment upgrades diff --git a/README.md b/README.md index aec9720..68bb257 100644 --- a/README.md +++ b/README.md @@ -1,1105 +1,130 @@ -# STEAM Security Dashboard +# STEAM Security Dashboard v1.0.0 -A self-hosted vulnerability management dashboard for the NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG business units. Centralises CVE tracking, Ivanti host finding triage, AEO compliance posture, FP and Archer exception workflows, and internal documentation in a single interface. +A self-hosted vulnerability management dashboard for the NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG business units. Centralises CVE tracking, Ivanti host finding triage, AEO compliance posture, FP/Archer/CARD exception workflows, and internal documentation in a single interface. ---- +## Quick Start -## Table of Contents +### Prerequisites -- [Overview](#overview) -- [Tech Stack](#tech-stack) -- [Prerequisites](#prerequisites) -- [Installation](#installation) -- [Configuration](#configuration) -- [Running the Application](#running-the-application) - - [Running as systemd services (auto-start on reboot)](#running-as-systemd-services-auto-start-on-reboot) -- [Features](#features) - - [Authentication and User Groups](#authentication-and-user-groups) - - [Home — CVE Management](#home--cve-management) - - [Reporting — Host Findings](#reporting--host-findings) - - [Ivanti Queue](#ivanti-queue) - - [Compliance — AEO Posture](#compliance--aeo-posture) - - [Knowledge Base](#knowledge-base) - - [Exports](#exports) - - [Jira Tickets](#jira-tickets) - - [Archer Risk Acceptance Tickets](#archer-risk-acceptance-tickets) - - [Admin Panel](#admin-panel) -- [Scripts](#scripts) -- [API Reference](#api-reference) -- [Architecture](#architecture) -- [Database Schema](#database-schema) -- [Security Model](#security-model) -- [Upgrading an Existing Deployment](#upgrading-an-existing-deployment) -- [Migrations](#migrations) -- [Troubleshooting](#troubleshooting) +- Node.js 18+ +- Python 3 with `python3-pandas` and `python3-openpyxl` (for compliance xlsx parsing) ---- - -## Overview - -The STEAM Security Dashboard answers a common problem in vulnerability management: tracking which CVEs have been addressed, whether supporting vendor documentation exists, where each finding is in the remediation or exception workflow, and how the team's overall AEO compliance posture is trending week over week. - -The application provides: - -- A searchable, filterable CVE list with per-vendor tracking and document storage -- NVD API integration to auto-populate CVE metadata -- **Ivanti/RiskSense integration** — sync open host findings with live FP workflow tracking -- **Reporting page** with donut charts, advanced per-column filtering, inline editing, Ivanti Queue, and CSV/XLSX export -- **Ivanti Queue** — personal staging list for batch-processing FP, Archer, and CARD workflows -- **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 -- Group-based access control (Admin, Standard_User, Leadership, Read_Only) with a full audit trail - ---- - -## Tech Stack - -| Layer | Technology | -|---|---| -| Backend | Node.js 18+, Express 5 | -| Database | SQLite3 | -| File uploads | Multer 2 | -| Auth | bcryptjs, cookie-based sessions, express-rate-limit | -| Frontend | React 19, lucide-react, recharts, xlsx, react-markdown, rehype-sanitize, mermaid | -| Compliance xlsx parsing | Python 3, pandas, openpyxl | - ---- - -## Prerequisites - -- Node.js 18 or later -- npm -- Python 3 with `python3-pandas` and `python3-openpyxl` apt packages (required for compliance xlsx parsing) - ---- - -## Installation - -### 1. Clone the repository +### Install ```bash git clone cd cve-dashboard -``` -### 2. Install backend dependencies - -```bash +# Backend dependencies npm install -``` -### 3. Install frontend dependencies +# Frontend dependencies +cd frontend && npm install && cd .. -```bash -cd frontend -npm install -``` - -### 4. Install Python dependencies - -Install via apt — this is the correct approach on Ubuntu/Debian and mirrors the dev server setup: - -```bash +# Python dependencies (Ubuntu/Debian) apt install -y python3-pandas python3-openpyxl ``` -> If apt packages are unavailable or you need a specific version, see `docs/python-venv-setup.md` for the venv fallback approach. - -> A bulk notes import script (`import_notes_from_csv.py`) is also available in `backend/scripts/` for maintenance tasks like backfilling notes from a CSV. It uses only Python stdlib. - -### 5. Configure environment variables - -Create `backend/.env` — the server will refuse to start without `SESSION_SECRET`: +### Configure ```bash -cd backend -cp .env.example .env -# Edit .env and set SESSION_SECRET to a random string: +cp backend/.env.example backend/.env +# Edit backend/.env — at minimum set SESSION_SECRET: # openssl rand -base64 32 ``` -See [Configuration](#configuration) for all available options. +See `backend/.env.example` for all available options including Ivanti API, Jira, and Atlas integration keys. -### 6. Initialize the database - -Run once from the `backend/` directory to create the SQLite database, all tables, indexes, and a default admin user: +### Initialize Database ```bash -cd backend -node setup.js +node backend/setup.js ``` -This creates `backend/cve_database.db` and generates a random admin password printed to stdout. **Save the password — it is only shown once.** +Creates the database with the complete schema and prints a one-time admin password. Save it. -### 7. Run database migrations - -Apply all feature migrations in order: +### Build and Run ```bash -cd backend -node migrations/add_knowledge_base_table.js -node migrations/add_archer_tickets_table.js -node migrations/add_ivanti_sync_table.js -node migrations/add_ivanti_findings_tables.js -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_todo_queue_hostname.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_fp_submissions_table.js -node migrations/add_user_groups.js -node migrations/add_created_by_columns.js -node migrations/add_fp_submission_editing.js -node migrations/add_granite_workflow_type.js -node migrations/add_compliance_notes_group_id.js +# Build frontend +cd frontend && npm run build && cd .. + +# Start servers +./start-servers.sh ``` -### 8. Build the frontend +Dashboard: http://localhost:3000 · API: http://localhost:3001 -```bash -cd frontend -npm run build -``` - -Or use `npm start` for the development server (see [Running the Application](#running-the-application)). - ---- - -## Configuration - -The application is configured via `.env` files. These files are gitignored and must be created manually per environment. - -### Backend: `backend/.env` - -```env -PORT=3001 -API_HOST=localhost -CORS_ORIGINS=http://YOUR_IP:3000 -SESSION_SECRET= -# NODE_ENV=production — see note below - -# Optional: NVD API key for higher rate limits (50 req/30s vs 5 req/30s) -# Register at https://nvd.nist.gov/developers/request-an-api-key -NVD_API_KEY=your-key-here - -# Ivanti / RiskSense integration (required for Reporting page sync) -IVANTI_API_KEY=your-ivanti-api-key -IVANTI_CLIENT_ID=1550 -# Optional: filter workflows to a specific person's submissions -IVANTI_FIRST_NAME= -IVANTI_LAST_NAME= -# Set to 'true' if your network has SSL inspection / self-signed certs -IVANTI_SKIP_TLS=false - -# Jira Data Center REST API (required for Jira Tickets page) -# VPN or Charter Network connection required for all Jira instances. -# Service accounts use Basic Auth (JIRA_API_USER + JIRA_API_TOKEN). -# PATs require ATLSUP approval — set JIRA_AUTH_METHOD=pat to use JIRA_PAT instead. -# Rate limits: 1440 requests/day, burst of 60/minute. -JIRA_BASE_URL=https://jira.charter.com -JIRA_AUTH_METHOD=basic -JIRA_API_USER=your-service-account -JIRA_API_TOKEN=your-api-token -# JIRA_PAT=your-pat-token -JIRA_PROJECT_KEY=VULN -JIRA_ISSUE_TYPE=Task -JIRA_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`. - -**`NODE_ENV` and the Secure cookie flag:** When `NODE_ENV=production`, session cookies are set with the `Secure` flag, which means the browser will only send them over HTTPS connections. If you are running the application over plain HTTP (no TLS/SSL), you **must** leave `NODE_ENV` unset or set it to `development` — otherwise login will succeed but every subsequent API request will return 401 because the browser silently drops the cookie. Only set `NODE_ENV=production` when the application is served behind HTTPS (e.g., via a reverse proxy with TLS termination). - -### Frontend: `frontend/.env` - -```env -REACT_APP_API_BASE=http://YOUR_IP:3001/api -REACT_APP_API_HOST=http://YOUR_IP:3001 -``` - -Replace `YOUR_IP` with the machine's IP address or hostname. Use `localhost` for local-only access. - -> **Important:** React caches environment variables at build/start time. After changing `frontend/.env`, fully restart the frontend process — a browser refresh alone is not sufficient. - ---- - -## Running the Application - -### Using the helper scripts (recommended) - -From the project root: - -```bash -./start-servers.sh # Starts backend and frontend in the background -./stop-servers.sh # Stops all servers -``` - -The start script saves PIDs to `backend.pid` and `frontend.pid`. Logs are written to `backend/backend.log` and `frontend/frontend.log`. - -### Running manually - -```bash -# Terminal 1 — backend -cd backend -node server.js - -# Terminal 2 — frontend (development server) -cd frontend -npm start -``` - -### Running as systemd services (auto-start on reboot) - -Two systemd unit files are installed to `/etc/systemd/system/` so the dashboard starts automatically when the server boots: - -| Unit | What it runs | -|---|---| -| `cve-backend.service` | `node server.js` from `backend/` | -| `cve-frontend.service` | `npm start` from `frontend/` (waits for backend) | - -Both services load their respective `.env` files, restart on failure (5-second delay), and append output to `backend/backend.log` and `frontend/frontend.log`. - -**First-time setup** (if the units are not yet installed): - -```bash -# Copy the unit files into systemd -cp systemd/cve-backend.service /etc/systemd/system/ -cp systemd/cve-frontend.service /etc/systemd/system/ - -# Reload systemd, enable on boot, and start -systemctl daemon-reload -systemctl enable --now cve-backend cve-frontend -``` - -**Common commands:** - -```bash -systemctl status cve-backend cve-frontend # Check both services -systemctl restart cve-backend # Restart backend only -systemctl restart cve-frontend # Restart frontend only -systemctl stop cve-backend cve-frontend # Stop both -journalctl -u cve-backend -f # Follow backend journal -journalctl -u cve-frontend -f # Follow frontend journal -``` - -> The helper scripts (`start-servers.sh` / `stop-servers.sh`) still work for ad-hoc use, but systemd is the recommended approach for persistent deployments. If switching to systemd, stop any script-launched processes first with `./stop-servers.sh` to avoid port conflicts. - -### Default ports - -| Service | URL | -|---|---| -| Frontend | http://localhost:3000 | -| Backend API | http://localhost:3001 | - ---- +For persistent deployments, use the systemd services in `systemd/`. See the full manual for setup instructions. ## Features -### Authentication and User Groups - -All routes require authentication. Four user groups are supported: - -| Group | Permissions | -|---|---| -| `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 | - -**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`. - ---- - -### Home — CVE Management - -The home page is the primary CVE research and tracking tool. - -**CVE List** -- Search CVEs by keyword (matches CVE ID, vendor, description) -- Filter by vendor, severity (Critical / High / Medium / Low), and status -- Color-coded severity badges: Critical (red), High (amber), Medium (sky blue), Low (green) -- Paginated list view - -**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 (ownership restrictions apply for Standard_User) -- The same CVE ID can be tracked across multiple vendors independently - -**Document Management** -- Upload documents attached to a CVE/vendor pair -- Supported document types: `advisory`, `email`, `screenshot`, `patch`, `other` -- Allowed file extensions: PDF, images (PNG, JPG, GIF, BMP, TIFF), Office documents (DOC, DOCX, XLS, XLSX, PPT, PPTX), text files (TXT, MD, CSV, LOG), email files (MSG, EML), and others (RTF, HTML, XML, JSON, YAML, ODF variants, ZIP, GZ, TAR, 7Z) -- File size limit: 10 MB per upload -- Admins can delete documents - -**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 (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 - -**Archer Ticket Quick Navigation** -- Archer EXC numbers shown on CVE rows -- Clicking an EXC badge navigates to the Reporting page pre-filtered to findings with that EXC number - -**Calendar Widget** -- Shows current month with red dot indicators on dates where Ivanti findings are due -- Click a date to navigate to the Reporting page filtered to that due date - ---- - -### Reporting — Host Findings - -The Reporting page is the core operational view for remediation tracking. It integrates with Ivanti/RiskSense to show all host findings for the configured business units. - -#### Syncing Data - -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.5–9.9 VRR) -2. Fetches the closed finding count separately -3. Sweeps closed findings to capture FP workflow states (including Approved FPs now closed) -4. Stores everything in the local SQLite cache - -Findings are also auto-synced on a 24-hour schedule. The last sync timestamp is shown at the top of the page. - -> `IVANTI_API_KEY` must be set in `backend/.env` for sync to work. - -#### Metric Charts - -| Chart | What it shows | -|---|---| -| **Open vs Closed** | Total open vs closed host findings direct from the Ivanti API | -| **Action Coverage** | Findings by action taken: FP Request · Archer Exception · Pending. Click a segment to filter the table. | -| **FP Finding Status** | How many *findings* are in each FP workflow state (Actionable, Requested, Reworked, Approved, Rejected, Expired) | -| **FP Workflow Status** | How many *unique FP# ticket IDs* are in each state — one ticket can cover many findings | - -#### Findings Table - -Each row represents a single Ivanti host finding. - -| Column | Description | -|---|---| -| Finding ID | Ivanti finding identifier | -| Severity | Numerical VRR score with group label (CRITICAL / HIGH) | -| Title | Vulnerability title | -| CVEs | Associated CVE IDs — up to 2 shown, remainder as "+N" badge | -| Host | Hostname — inline editable | -| IP Address | Host IP address | -| DNS | DNS/FQDN — inline editable | -| Due Date | Remediation due date; red if overdue, amber if within 30 days | -| SLA | SLA status: OVERDUE / AT_RISK / WITHIN_SLA | -| BU | Business unit | -| Workflow | FP# ticket ID and state badge — colour-coded by urgency | -| 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. Requires Admin or Standard_User group. - -**CVE Tooltips:** Hover over any CVE badge in the table to see a tooltip with the CVE description and severity (if the CVE exists in the local database). Tooltips appear after a 300ms delay, are cached in memory for the session, and auto-position to stay within the viewport. - -**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`. - -**Row visibility:** Hide individual rows by clicking the `EyeOff` icon on any row, or select multiple rows via checkboxes and click **Hide Selected** in the bulk action toolbar. Hidden rows are excluded from the table, the Action Coverage chart, and exports. Use the **Hidden (N)** button in the toolbar to view and restore hidden rows individually or all at once. Hidden row state persists to `localStorage` across sessions. Row hiding is a personal view preference available to all user groups. - -**Export:** Click **Export** to download the current filtered view as CSV or XLSX. Hidden rows and filtered rows are both excluded from exports. 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. 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") -- For **CARD** items: no vendor entry required — the IP address is captured automatically -- Select the workflow type: **FP**, **Archer**, or **CARD** -- Click **Add to Queue** — the row checkbox turns solid blue - -**Queue panel:** Click the **Queue** button (top right of Reporting page) to open the slide-out panel: -- **CARD** items appear at the top in their own section with the IP address displayed -- **FP and Archer** items are grouped alphabetically by vendor below -- Badges show workflow type: amber = FP, sky = Archer, green = CARD - -**Working the queue:** -- Check the green checkbox on an item to mark it complete (strikethrough at reduced opacity) -- Delete individual items with the trash icon, or select multiple and use **Delete (N)** -- **Clear Completed** removes all marked-complete items at once -- **Create FP Workflow** — select pending FP items and click to open the FP Workflow modal, which submits a False Positive workflow batch directly to the Ivanti API with form fields, file attachments, and scope override. Attachments can be local file uploads or documents selected from the CVE document library — library documents are read from disk and sent to Ivanti identically to local uploads. Successful submission marks the queue items as complete and records the submission locally. - -**Redirecting completed items:** -- Completed items show a redirect button (↱) next to the delete icon -- Click redirect to open a modal where you select the target workflow type (FP, Archer, or CARD) and vendor (required for FP/Archer) -- Redirecting creates a new pending queue item with the same finding data under the new workflow type — the original completed item is preserved -- This is useful when a CARD inventory fix is done but the finding still needs an FP or Archer workflow, or when an item was assigned to the wrong workflow initially -- Not every completed item needs a redirect — it's an optional action for items that require further processing - -Queue items are stored in the database, are **personal to your login**, and persist across sessions and page refreshes. - ---- - -### Compliance — AEO Posture - -The Compliance page tracks NTS-AEO team posture against the AEO compliance framework using weekly xlsx reports exported from the NTS_AEO reporting system. - -#### Upload Workflow - -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 backend extracts the xlsx schema and runs a **drift check** against the parser configuration (`compliance_config.json`). If structural drift is detected, a drift review phase is shown before the diff preview: - - **Breaking** findings (red) — missing core columns or detail sheets — block the upload until the config is updated - - **Silent-miss** findings (amber) — unknown metrics or sheets that will be miscategorised — warn but allow proceeding - - **Cosmetic** findings (muted) — new columns or stale config entries — informational only - - Admins can click **Reconcile Config** to auto-patch the parser configuration and re-run the check -3. If no breaking drift exists, the **diff preview** is shown — new violations, resolved items, and recurring items since the last upload -4. Click **Confirm Upload** to commit. The upload is recorded and the device table updates immediately. - -The report date is extracted automatically from the filename. - -**Upload rollback:** Admins can roll back the most recent upload via `POST /api/compliance/rollback/:uploadId`. Rolling back deletes new items introduced by that upload, re-activates items it resolved, and decrements seen counts on recurring items. - -#### Metric Health Cards - -Each AEO metric (e.g., `2.3.4i`, `5.2.4`) is shown as a health card displaying: -- Compliance percentage vs target -- Status: Meets/Exceeds Target · Within 15% of Target · Below 15% of Target - -Click a card to filter the device table to only devices failing that metric. - -#### Device Table - -Shows all devices currently failing one or more metrics (Active tab) or previously resolved (Resolved tab). Columns: Hostname, IP Address, Type, Failing Metrics, Times Seen. Click a row to open the detail panel. - -#### Detail Panel - -A slide-out panel for a selected device showing: -- **Failing Metrics** — each metric with surfaced extra fields (CVEs, SLA status, due date, OS, EoL, Splunk last seen, MFA software) -- 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 can be deleted by the author or an Admin — deleting a multi-metric note removes it from all linked metrics. Requires Admin or Standard_User group. - -Notes persist across uploads and are keyed to the device hostname and metric ID. - -#### Teams - -Only **STEAM** and **ACCESS-ENG** teams are tracked. The team selector at the top of the page switches context between them. - ---- - -### Knowledge Base - -A document library for internal reference material — policies, runbooks, vendor advisories, and process guides. - -- 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 -- 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). - ---- - -### Exports - -Bulk export tools for reports and data extracts. Available to Admin, Standard_User, and Leadership groups. Read_Only users cannot access the Exports page. - ---- - -### Jira Tickets - -A dedicated page for managing Jira Data Center tickets linked to CVE/vendor pairs. Accessible from the navigation drawer. Requires a configured Jira API connection (see [Configuration](#configuration)). - -**Ticket list** -- View all tracked Jira tickets with status, CVE ID, vendor, summary, and Jira key -- Filter by status or search by keyword -- Click a Jira key to open the issue in Jira Data Center - -**Jira API operations (Admin/Standard_User)** -- **Lookup** — search for any Jira issue by key and view its current status, assignee, and summary -- **Create in Jira** — create a new Jira issue directly from the dashboard with project key, issue type, summary, and description; the resulting ticket is automatically linked to a CVE/vendor pair in the local database -- **Sync** — refresh a single ticket's status and summary from Jira, or bulk-sync all tracked tickets via JQL search -- **Create / Edit / Delete** — manage local ticket records linking Jira keys to CVE/vendor pairs - -**Connection test (Admin)** — verify Jira API credentials and connectivity from the page header. - -**Rate limit monitoring (Admin)** — view current burst and daily rate limit usage against Charter's posted limits (60/minute burst, 1 440/day). - -All Jira API calls are proxied through the backend. Credentials are never exposed to the browser. Rate limits are enforced client-side with inter-request delays (1s for GETs, 2s for writes). See `docs/jira-api-use-cases.md` for the full API compliance summary. - ---- - -### Archer Risk Acceptance Tickets - -Track Archer exception tickets (EXC numbers) linked to specific CVE/vendor pairs. - -- EXC number format: `EXC-NNNNN` -- Statuses: `Draft`, `Open`, `Under Review`, `Accepted` -- 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) - ---- - -### Admin Panel - -The Admin Panel is a full-page, tabbed interface accessible only to Admin-group users. It replaces the previous inline modal rendering and follows the dashboard's dark tactical intelligence theme. Three tabs provide consolidated access to administrative functions: - -**User Management** — the default tab. Displays a themed user table with group badges (Admin in red, Standard_User in accent blue, Leadership in amber, Read_Only in muted grey). Admins can create, edit, and delete users, change group assignments, and toggle active status — all through inline forms styled to match the dashboard. Admins cannot demote themselves or deactivate their own account. Deactivating a user immediately invalidates all their active sessions. All group changes are audit-logged with previous and new group values. - -**Audit Log** — a paginated, filterable log table showing every state-changing action with timestamp, username, action type, entity type, entity ID, details, and IP address. Action types are colour-coded: login in green, delete in red, create in accent blue, update in amber. Filter by username, action type, entity type, and date range. Results are paginated at 25 per page. - -**System Info** — stat cards showing total user count, active user count, total audit log entries, and users who logged in within the last 7 days. A "Recent Activity" section lists the 10 most recent audit log entries. - -The `UserMenu` quick-access links ("Manage Users", "Audit Log") continue to open the existing modal components for fast access without navigating to the admin page. - ---- - -## Scripts - -### `backend/scripts/parse_compliance_xlsx.py` - -Called automatically by the compliance upload flow. Parses the NTS_AEO xlsx report and outputs structured JSON to stdout for consumption by the Node compliance route. Reads metric categories, core columns, and skip sheets from `compliance_config.json` (shared with the drift checker). - -- Reads all detail sheets; skips sheets listed in `skip_sheets` -- Filters to rows where `Compliant == False` -- Extracts hostname, IP, device type, team, and metric ID per row -- Captures all non-core columns in `extra_json` (CVEs, SLA status, OS, EoL, Splunk, MFA, Ivanti_Vulnerability_ID, etc.) -- Parses `Summary` sheet for per-team metric health (compliance_pct, target, status) -- Extracts report date from the filename (`NTS_AEO_YYYY_MM_DD.xlsx`) - -**Dependencies:** `pandas>=2.0.0`, `openpyxl>=3.0.0` - -### `backend/scripts/extract_xlsx_schema.py` - -Called by the preview endpoint before parsing. Extracts the structural schema of an xlsx file as JSON — sheet names, first-row column headers per sheet, and unique metric values from the Summary sheet. The Node.js drift checker compares this schema against `compliance_config.json` to detect breaking, silent-miss, and cosmetic drift. - -**Dependencies:** `openpyxl>=3.0.0` - -### `backend/scripts/compliance_config.json` - -Shared parser configuration file — the single source of truth for `metric_categories` (metric ID → category mapping), `core_cols` (columns that become main item fields), and `skip_sheets` (sheets excluded from parsing). Read by both `parse_compliance_xlsx.py` and the Node.js `driftChecker.js` module. Admins can auto-patch this file via the **Reconcile Config** button in the upload modal. - ---- - -## API Reference - -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 | Group | Description | -|---|---|---|---| -| 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` | Any | Get current user info (returns `group` field) | -| POST | `/api/auth/cleanup-sessions` | Admin | Delete expired sessions | - -### CVEs - -| Method | Path | Group | Description | -|---|---|---|---| -| 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 | -| GET | `/api/cves/:cveId/tooltip` | Any | Get CVE description and severity for tooltip display (truncated to 300 chars) | - -### Documents - -| Method | Path | Group | Description | -|---|---|---|---| -| 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 | Group | Description | -|---|---|---|---| -| 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) | -| GET | `/api/jira-tickets/connection-test` | Admin | Test Jira API connectivity and credentials | -| GET | `/api/jira-tickets/rate-limit` | Admin | Get current burst and daily rate limit usage | -| GET | `/api/jira-tickets/lookup/:issueKey` | Any | Look up a single Jira issue by key | -| POST | `/api/jira-tickets/search` | Any | JQL search for Jira issues | -| POST | `/api/jira-tickets/create-in-jira` | Admin, Standard_User | Create an issue in Jira and link it locally | -| POST | `/api/jira-tickets/sync-all` | Admin | Bulk-sync all tracked tickets via JQL | -| POST | `/api/jira-tickets/:id/sync` | Admin, Standard_User | Sync a single ticket's status from Jira | - -### Ivanti — Host Findings - -| Method | Path | Group | Description | -|---|---|---|---| -| 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 | Group | Description | -|---|---|---|---| -| GET | `/api/ivanti/workflows` | Any | Get cached workflow data | -| POST | `/api/ivanti/workflows/sync` | Admin, Standard_User | Trigger an immediate workflow sync | - -### Ivanti — FP Workflow Submission - -| Method | Path | Group | Description | -|---|---|---|---| -| GET | `/api/ivanti/fp-workflow/documents/search` | Any | Search the CVE document library by name, CVE ID, or vendor; returns up to 50 matches | -| POST | `/api/ivanti/fp-workflow` | Admin, Standard_User | Submit an FP workflow batch to Ivanti API (multipart/form-data with local attachments and/or `libraryDocIds`) | -| GET | `/api/ivanti/fp-workflow/submissions` | Any | List FP submissions for the current user | -| PUT | `/api/ivanti/fp-workflow/submissions/:id` | Admin, Standard_User | Update an FP submission (edit form fields) | -| POST | `/api/ivanti/fp-workflow/submissions/:id/findings` | Admin, Standard_User | Add or remove findings on an existing submission | -| POST | `/api/ivanti/fp-workflow/submissions/:id/attachments` | Admin, Standard_User | Upload additional attachments (local files and/or `libraryDocIds`) to an existing submission | -| PATCH | `/api/ivanti/fp-workflow/submissions/:id/status` | Admin, Standard_User | Update submission lifecycle status | - -### Ivanti — Todo Queue - -| Method | Path | Group | Description | -|---|---|---|---| -| 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 | -| POST | `/api/ivanti/todo-queue/batch` | Admin, Standard_User | Batch-add multiple findings to the queue | -| PUT | `/api/ivanti/todo-queue/:id` | Admin, Standard_User | Update a queue item (mark complete, edit vendor/type) | -| POST | `/api/ivanti/todo-queue/:id/redirect` | Admin, Standard_User | Redirect a completed item to a different workflow 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 | Group | Description | -|---|---|---|---| -| POST | `/api/compliance/preview` | Admin, Standard_User | Parse an xlsx upload, run drift check, and return drift report + diff + temp file path | -| POST | `/api/compliance/commit` | Admin, Standard_User | Commit a previewed upload to the database | -| POST | `/api/compliance/reconcile-config` | Admin | Auto-patch `compliance_config.json` to resolve breaking and silent-miss drift findings | -| POST | `/api/compliance/rollback/:uploadId` | Admin | Roll back the most recent upload (deletes new items, re-activates resolved items) | -| 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; accepts `metric_ids` array for multi-metric notes | -| DELETE | `/api/compliance/notes/:id` | Admin, Standard_User | Delete a note by ID; `?group=true` deletes all notes sharing the same `group_id`. Author or Admin only. | - -### Knowledge Base - -| Method | Path | Group | Description | -|---|---|---|---| -| 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 | Group | Description | -|---|---|---|---| -| 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 | 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 | - -### Audit Logs (Admin only) - -| 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 | - -### Utility - -| Method | Path | Group | Description | -|---|---|---|---| -| GET | `/api/vendors` | Any | List all distinct vendor names | -| GET | `/api/stats` | Any | Dashboard statistics | - ---- - -## Architecture +| Feature | Description | +|---------|-------------| +| **CVE Management** | Track CVEs across multiple vendors with document storage and NVD auto-fill | +| **Reporting** | Ivanti host finding triage with donut charts, inline editing, advanced filtering, CSV/XLSX export | +| **Ivanti Queue** | Personal staging list for batch FP, Archer, CARD, and Granite workflows | +| **FP Workflow** | Submit false positive workflows directly to Ivanti API with attachments | +| **Compliance** | Weekly AEO xlsx upload with diff preview, drift detection, per-team metric health cards | +| **Archive Tracking** | Automatic detection of disappeared/returned findings with BU reassignment classification | +| **Findings Trend** | Historical open vs closed chart with archive activity sparkline and shift reason tooltips | +| **Jira Integration** | Create, sync, and track Jira Data Center tickets linked to CVE/vendor pairs | +| **Archer Tickets** | Track risk acceptance exceptions (EXC numbers) linked to findings | +| **CARD API** | Granite/CARD asset lookup integration for network device workflows | +| **Knowledge Base** | Internal document library with inline PDF/Markdown viewing | +| **Access Control** | Four user groups (Admin, Standard_User, Leadership, Read_Only) with full audit trail | + +## Project Structure ``` cve-dashboard/ -├── start-servers.sh # Start backend + frontend in background -├── stop-servers.sh # Stop all servers -├── package.json # Root package.json (backend dependencies) -├── systemd/ # systemd unit files for auto-start on boot -│ ├── cve-backend.service -│ └── cve-frontend.service -│ ├── backend/ -│ ├── server.js # Express app — routes, middleware, security headers -│ ├── setup.js # One-time DB initialization and default admin creation -│ ├── cve_database.db # SQLite database (gitignored) -│ ├── uploads/ # File storage root (gitignored) -│ │ ├── // # CVE documents -│ │ ├── knowledge_base/ # Knowledge base documents -│ │ └── temp/ # Temporary upload staging -│ ├── routes/ -│ │ ├── auth.js # Login, logout, session check, rate limiting -│ │ ├── users.js # User CRUD (admin) -│ │ ├── auditLog.js # Audit log viewer (admin) -│ │ ├── nvdLookup.js # NVD API proxy -│ │ ├── knowledgeBase.js # Knowledge base document management -│ │ ├── archerTickets.js # Archer EXC ticket CRUD -│ │ ├── 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 -│ │ ├── jiraTickets.js # Jira ticket CRUD + Jira REST API integration (lookup, sync, create) -│ │ └── compliance.js # AEO compliance upload, diff, device tracking, notes -│ ├── middleware/ -│ │ └── auth.js # requireAuth and requireGroup middleware -│ ├── helpers/ -│ │ ├── auditLog.js # logAudit helper (fire-and-forget) -│ │ ├── driftChecker.js # Schema drift detection: compareSchemaToDrift(), loadConfig(), reconcileConfig() -│ │ ├── ivantiApi.js # Ivanti API HTTP helpers (multipart, JSON, form POST) -│ │ └── jiraApi.js # Jira Data Center REST API helpers (Basic/PAT auth, rate limiting) -│ ├── migrations/ # Sequential migration scripts (run manually with node) -│ └── scripts/ -│ ├── compliance_config.json # Shared parser config (metric_categories, core_cols, skip_sheets) -│ ├── extract_xlsx_schema.py # Extracts xlsx structure as JSON for drift checking -│ ├── parse_compliance_xlsx.py # Parses NTS_AEO xlsx compliance reports -│ ├── import_notes_from_csv.py # Bulk-import finding notes from CSV -│ └── requirements.txt # pandas, openpyxl -│ -└── frontend/ - └── src/ - ├── App.js # Home dashboard — CVE list, filters, modals, calendar - ├── App.css # Global styles and CSS variables - ├── contexts/ - │ └── AuthContext.js # Auth state provider (login, logout, group helpers) - └── components/ - ├── LoginForm.js # Login page - ├── NavDrawer.js # Side navigation drawer (pages + 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 modal (quick-access from UserMenu) - ├── AuditLog.js # Admin audit log modal (quick-access from UserMenu) - ├── NvdSyncModal.js # Bulk NVD sync dialog - ├── KnowledgeBaseModal.js # Knowledge base upload/list modal - ├── KnowledgeBaseViewer.js # Inline document viewer (sandboxed iframe, sanitized markdown) - ├── ConfirmModal.js # Themed confirmation dialog (replaces window.confirm) - ├── CveTooltip.js # Hover tooltip for CVE badges (portal-rendered, cached) - ├── RedirectModal.js # Queue item redirect modal (workflow type + vendor selection) - └── pages/ - ├── AdminPage.js # Admin panel: user management, audit log, system info - ├── 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 - ├── JiraPage.js # Jira ticket management and Jira API integration - ├── KnowledgeBasePage.js # Knowledge base page - └── ExportsPage.js # Exports page (group-gated) +│ ├── server.js # Express API server +│ ├── setup.js # Database initialization (run once) +│ ├── routes/ # API route handlers +│ ├── helpers/ # API clients (Ivanti, Jira, Atlas, CARD) +│ ├── middleware/ # Auth middleware +│ ├── migrations/ # Schema migrations (for existing deployments) +│ └── scripts/ # Compliance parser, data import utilities +├── frontend/ +│ ├── src/ +│ │ ├── App.js # Main app with routing +│ │ ├── components/ # React components +│ │ └── contexts/ # Auth context +│ └── public/ +├── docs/ +│ ├── api/ # API specs (Ivanti, Atlas, Jira) +│ ├── design/ # Design system, workflow diagrams +│ ├── guides/ # User guides, full reference manual +│ ├── security/ # Security audits and remediation plans +│ ├── testing/ # Test plans and scripts +│ └── troubleshooting/ # Investigation scripts and reports +├── systemd/ # systemd service files +├── start-servers.sh +└── stop-servers.sh ``` +## Tech Stack + +| Layer | Technology | +|-------|------------| +| Backend | Node.js 18+, Express 5, SQLite3 | +| Frontend | React 19, Recharts, Lucide React | +| Auth | bcryptjs, cookie-based sessions, express-rate-limit | +| Compliance | Python 3, pandas, openpyxl | + +## Documentation + +- **[Full Reference Manual](docs/guides/full-reference-manual.md)** — comprehensive feature documentation, API reference, database schema, security model, and configuration details +- **[Migration Guide](backend/migrations/README.md)** — schema migration scripts for upgrading existing deployments +- **[Design System](docs/design/design-system.md)** — UI component patterns and color system +- **[Ivanti API Reference](docs/api/ivanti-api-reference.md)** — Ivanti/RiskSense API integration details +- **[Jira API Use Cases](docs/api/jira-api-use-cases.md)** — Jira Data Center API compliance summary + +## License + +Internal use only — Charter Communications / NTS-AEO. + --- -## Database Schema - -### Core tables (created by `setup.js`) - -**`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)` with `ON DELETE CASCADE`. - -**`required_documents`** — Vendor-specific document requirements. - -**`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. - -**`audit_logs`** — Append-only log of all state-changing actions. - -### Feature tables (added by migrations) - -**`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)`. 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. - -**`ivanti_findings_cache`** — Single-row cache for Ivanti host findings. - -**`ivanti_finding_notes`** — Persistent per-finding notes keyed by finding ID. Survives cache refreshes. `UNIQUE(finding_id)`. - -**`ivanti_counts_cache`** — Single-row cache for finding metrics: open/closed counts, FP workflow state breakdowns by finding and by unique ticket ID. - -**`ivanti_finding_overrides`** — Editor-applied overrides for `hostName` and `dns` fields. `UNIQUE(finding_id, field)`. - -**`ivanti_todo_queue`** — Personal per-user queue of findings staged for FP, Archer, or CARD processing. Keyed by `(user_id, finding_id)`. Completed items can be redirected to a different workflow type via `POST /:id/redirect`, which creates a new pending item preserving the original finding data. - -**`ivanti_fp_submissions`** — Record of FP workflow submissions to the Ivanti API. Tracks user, workflow batch ID, form fields, finding IDs, queue item IDs, attachment results, and submission status (success/partial/failed). - -**`compliance_uploads`** — Record of each compliance xlsx upload: filename, report date, uploader, timestamp, and new/resolved/recurring counts. - -**`compliance_items`** — One row per device/metric violation. Tracks hostname, IP, device type, team, metric ID, category, `extra_json` (all non-core xlsx columns), status (active/resolved), first seen upload, and times seen. Identity key: `(hostname, metric_id)`. - -**`compliance_notes`** — Timestamped notes per hostname/metric. Multiple notes per combination are supported. `group_id` column links notes created in the same multi-metric submission. Foreign-key linked to compliance items. - -### View - -**`cve_document_status`** — Aggregates document counts per CVE/vendor and derives a `compliance_status` (`Complete` when an advisory is present, otherwise `Missing Required Docs`). - ---- - -## 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,}$/` -- Severity must be one of: `Critical`, `High`, `Medium`, `Low` -- 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 - -### Security headers - -Applied to all responses: - -- `X-Content-Type-Options: nosniff` -- `X-Frame-Options: SAMEORIGIN` -- `X-XSS-Protection: 1; mode=block` -- `Referrer-Policy: strict-origin-when-cross-origin` -- `Permissions-Policy: camera=(), microphone=(), geolocation=()` - ---- - -## Upgrading an Existing Deployment - -This procedure updates the application code and schema while preserving all existing data. The database file (`backend/cve_database.db`) is never overwritten by `git pull` — it is gitignored. - -```bash -# 1. Stop the running servers -cd /home/cve-dashboard -./stop-servers.sh - -# 2. Pull latest code -git pull origin master - -# 3. Install backend dependencies (picks up any new packages) -npm install - -# 4. Install frontend dependencies -cd frontend -npm install -cd .. - -# 5. Ensure SESSION_SECRET is set in backend/.env -# If missing: -# echo "SESSION_SECRET=$(openssl rand -base64 32)" >> backend/.env - -# 6. Run all migrations (idempotent — safe to re-run, skips already-applied changes) -cd backend -node migrations/add_knowledge_base_table.js -node migrations/add_archer_tickets_table.js -node migrations/add_ivanti_sync_table.js -node migrations/add_ivanti_findings_tables.js -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_todo_queue_hostname.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_fp_submissions_table.js -node migrations/add_user_groups.js -node migrations/add_created_by_columns.js -node migrations/add_fp_submission_editing.js -node migrations/add_granite_workflow_type.js -node migrations/add_compliance_notes_group_id.js -cd .. - -# 7. Rebuild the frontend -cd frontend -npm run build -cd .. - -# 8. Start servers -./start-servers.sh -# Or, if using systemd services: -# systemctl restart cve-backend cve-frontend -``` - -After upgrading, clear your browser cookies and log in fresh — session format changes between versions will invalidate old sessions. - -> **Do not re-run `node setup.js`** on an existing deployment. It is only for first-time initialization. Re-running it will not destroy data (it checks for existing tables/users), but it is unnecessary and may create a duplicate admin account. - -> **NODE_ENV reminder:** If you are running over plain HTTP (no TLS), make sure `NODE_ENV` is **not** set to `production` in `backend/.env`. See [Troubleshooting](#troubleshooting) for details. - ---- - -## Migrations - -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 -node migrations/add_knowledge_base_table.js -node migrations/add_archer_tickets_table.js -node migrations/add_ivanti_sync_table.js -node migrations/add_ivanti_findings_tables.js -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_todo_queue_hostname.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_fp_submissions_table.js -node migrations/add_user_groups.js -node migrations/add_created_by_columns.js -node migrations/add_fp_submission_editing.js -node migrations/add_granite_workflow_type.js -node migrations/add_compliance_notes_group_id.js -``` - -For deployments upgrading from an older schema, the following legacy migration scripts are also available in `backend/`: - -- `migrate_multivendor.js` — Adds multi-vendor support to an older single-vendor schema -- `migrate-audit-log.js` — Adds the `audit_logs` table to pre-auth deployments -- `migrate-to-1.1.js` — General 1.0 → 1.1 schema update - -> Several columns (`fp_workflow_counts_json`, `fp_id_counts_json`, `seen_count`, `summary_json`) are added automatically via idempotent `ALTER TABLE` statements each time the server starts. No manual re-run is needed. - ---- - -## Troubleshooting - -### Login succeeds but all pages show "Error Loading" / 401 Unauthorized - -**Symptom:** You can log in successfully, but the dashboard shows "Error Loading CVEs", "Failed to fetch", and the browser console shows 401 on every API call. - -**Cause:** The session cookie has the `Secure` flag set (because `NODE_ENV=production` in `backend/.env`), but the application is being accessed over plain HTTP. Browsers silently refuse to send `Secure` cookies over non-HTTPS connections, so every request after login arrives without a session cookie. - -**Fix:** Either: -1. Remove `NODE_ENV=production` from `backend/.env` (or set it to `development`) and restart the backend, **or** -2. Set up HTTPS (e.g., via nginx reverse proxy with TLS termination) and access the app over `https://` - -### Login fails with "Too many login attempts" - -**Cause:** The login endpoint is rate-limited to 20 attempts per 15-minute window. Wait 15 minutes or restart the backend to reset the counter. - -### Server refuses to start: "SESSION_SECRET environment variable must be set" - -**Fix:** Add a `SESSION_SECRET` to `backend/.env`: -```bash -echo "SESSION_SECRET=$(openssl rand -base64 32)" >> backend/.env -``` - -### After upgrading: "user_group" errors or missing group data - -**Fix:** Run the group migration: -```bash -cd backend -node migrations/add_user_groups.js -node migrations/add_created_by_columns.js -``` -This maps existing roles to groups automatically (admin→Admin, editor→Standard_User, viewer→Read_Only). +*Designed and built by Jordan Ramos (jordan.ramos@spectrum.com)* diff --git a/backend/.env.example b/backend/.env.example index 5932d65..d13d620 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,6 +3,10 @@ PORT=3001 API_HOST=localhost CORS_ORIGINS=http://localhost:3000 +# Session secret — REQUIRED. Server will not start without this. +# Generate with: openssl rand -base64 32 +SESSION_SECRET= + # NVD API Key (optional - increases rate limit from 5 to 50 requests per 30s) # Request one at https://nvd.nist.gov/developers/request-an-api-key NVD_API_KEY= diff --git a/backend/migrate-audit-log.js b/backend/migrate-audit-log.js deleted file mode 100644 index 799d734..0000000 --- a/backend/migrate-audit-log.js +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env node -// Migration script: Add audit_logs table -// Run: node migrate-audit-log.js - -const sqlite3 = require('sqlite3').verbose(); -const fs = require('fs'); - -const DB_FILE = './cve_database.db'; -const BACKUP_FILE = `./cve_database_backup_${Date.now()}.db`; - -function run(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.run(sql, params, function(err) { - if (err) reject(err); - else resolve(this); - }); - }); -} - -function get(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.get(sql, params, (err, row) => { - if (err) reject(err); - else resolve(row); - }); - }); -} - -async function migrate() { - console.log('╔════════════════════════════════════════════════════════╗'); - console.log('║ CVE Database Migration: Add Audit Logs ║'); - console.log('╚════════════════════════════════════════════════════════╝\n'); - - if (!fs.existsSync(DB_FILE)) { - console.log('❌ Database not found. Run setup.js for fresh install.'); - process.exit(1); - } - - // Backup database - console.log('📦 Creating backup...'); - fs.copyFileSync(DB_FILE, BACKUP_FILE); - console.log(` ✓ Backup saved to: ${BACKUP_FILE}\n`); - - const db = new sqlite3.Database(DB_FILE); - - try { - // Check if table already exists - const exists = await get(db, - "SELECT name FROM sqlite_master WHERE type='table' AND name='audit_logs'" - ); - - if (exists) { - console.log('⏭️ audit_logs table already exists, nothing to do.'); - } else { - console.log('1️⃣ Creating audit_logs table...'); - await run(db, ` - CREATE TABLE audit_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - username VARCHAR(50) NOT NULL, - action VARCHAR(50) NOT NULL, - entity_type VARCHAR(50) NOT NULL, - entity_id VARCHAR(100), - details TEXT, - ip_address VARCHAR(45), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `); - console.log(' ✓ Table created'); - - console.log('2️⃣ Creating indexes...'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at)'); - console.log(' ✓ Indexes created'); - } - - console.log('\n╔════════════════════════════════════════════════════════╗'); - console.log('║ MIGRATION COMPLETE! ║'); - console.log('╚════════════════════════════════════════════════════════╝'); - console.log('\n📋 Summary:'); - console.log(' ✓ audit_logs table ready'); - console.log(`\n💾 Backup saved: ${BACKUP_FILE}`); - console.log('\n🚀 Restart your server to apply changes.\n'); - - } catch (error) { - console.error('\n❌ Migration failed:', error.message); - console.log(`\n🔄 To restore from backup: cp ${BACKUP_FILE} ${DB_FILE}`); - process.exit(1); - } finally { - db.close(); - } -} - -migrate(); diff --git a/backend/migrate-to-1.1.js b/backend/migrate-to-1.1.js deleted file mode 100755 index ed88338..0000000 --- a/backend/migrate-to-1.1.js +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env node -// Migration script: v1.0.0 -> v1.1.0 -// Adds: users, sessions tables, multi-vendor support, vendor column in documents -// Run: node migrate-to-1.1.js - -const sqlite3 = require('sqlite3').verbose(); -const bcrypt = require('bcryptjs'); -const fs = require('fs'); -const path = require('path'); - -const DB_FILE = './cve_database.db'; -const BACKUP_FILE = `./cve_database_backup_${Date.now()}.db`; - -async function migrate() { - console.log('╔════════════════════════════════════════════════════════╗'); - console.log('║ CVE Database Migration: v1.0.0 → v1.1.0 ║'); - console.log('╚════════════════════════════════════════════════════════╝\n'); - - // Check if database exists - if (!fs.existsSync(DB_FILE)) { - console.log('❌ Database not found. Run setup.js for fresh install.'); - process.exit(1); - } - - // Backup database - console.log('📦 Creating backup...'); - fs.copyFileSync(DB_FILE, BACKUP_FILE); - console.log(` ✓ Backup saved to: ${BACKUP_FILE}\n`); - - const db = new sqlite3.Database(DB_FILE); - - try { - // Run migrations in sequence - await addUsersTable(db); - await addSessionsTable(db); - await addVendorToDocuments(db); - await updateCvesConstraint(db); - await createDefaultAdmin(db); - await updateView(db); - - console.log('\n╔════════════════════════════════════════════════════════╗'); - console.log('║ MIGRATION COMPLETE! ║'); - console.log('╚════════════════════════════════════════════════════════╝'); - console.log('\n📋 Summary:'); - console.log(' ✓ Users table added'); - console.log(' ✓ Sessions table added'); - console.log(' ✓ Vendor column added to documents'); - console.log(' ✓ Multi-vendor constraint applied to cves'); - console.log(' ✓ Default admin user created (admin/admin123)'); - console.log(`\n💾 Backup saved: ${BACKUP_FILE}`); - console.log('\n🚀 Restart your server to apply changes.\n'); - - } catch (error) { - console.error('\n❌ Migration failed:', error.message); - console.log(`\n🔄 To restore from backup: cp ${BACKUP_FILE} ${DB_FILE}`); - process.exit(1); - } finally { - db.close(); - } -} - -function run(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.run(sql, params, function(err) { - if (err) reject(err); - else resolve(this); - }); - }); -} - -function get(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.get(sql, params, (err, row) => { - if (err) reject(err); - else resolve(row); - }); - }); -} - -function all(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.all(sql, params, (err, rows) => { - if (err) reject(err); - else resolve(rows); - }); - }); -} - -async function addUsersTable(db) { - console.log('1️⃣ Adding users table...'); - - const exists = await get(db, - "SELECT name FROM sqlite_master WHERE type='table' AND name='users'" - ); - - if (exists) { - console.log(' ⏭️ Users table already exists, skipping'); - return; - } - - await run(db, ` - CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username VARCHAR(50) UNIQUE NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - role VARCHAR(20) NOT NULL DEFAULT 'viewer', - is_active BOOLEAN DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_login TIMESTAMP, - CHECK (role IN ('admin', 'editor', 'viewer')) - ) - `); - - await run(db, 'CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)'); - console.log(' ✓ Users table created'); -} - -async function addSessionsTable(db) { - console.log('2️⃣ Adding sessions table...'); - - const exists = await get(db, - "SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'" - ); - - if (exists) { - console.log(' ⏭️ Sessions table already exists, skipping'); - return; - } - - await run(db, ` - CREATE TABLE sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id VARCHAR(255) UNIQUE NOT NULL, - user_id INTEGER NOT NULL, - expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ) - `); - - await run(db, 'CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)'); - console.log(' ✓ Sessions table created'); -} - -async function addVendorToDocuments(db) { - console.log('3️⃣ Adding vendor column to documents...'); - - // Check if vendor column exists - const columns = await all(db, "PRAGMA table_info(documents)"); - const hasVendor = columns.some(col => col.name === 'vendor'); - - if (hasVendor) { - console.log(' ⏭️ Vendor column already exists, skipping'); - return; - } - - // Add vendor column - await run(db, "ALTER TABLE documents ADD COLUMN vendor VARCHAR(100)"); - - // Populate vendor from the cves table based on cve_id - await run(db, ` - UPDATE documents - SET vendor = ( - SELECT c.vendor - FROM cves c - WHERE c.cve_id = documents.cve_id - LIMIT 1 - ) - WHERE vendor IS NULL - `); - - // Set default for any remaining nulls - await run(db, "UPDATE documents SET vendor = 'Unknown' WHERE vendor IS NULL"); - - await run(db, 'CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor)'); - console.log(' ✓ Vendor column added and populated'); -} - -async function updateCvesConstraint(db) { - console.log('4️⃣ Updating CVEs table for multi-vendor support...'); - - // Check current schema - const tableInfo = await get(db, - "SELECT sql FROM sqlite_master WHERE type='table' AND name='cves'" - ); - - if (tableInfo.sql.includes('UNIQUE(cve_id, vendor)')) { - console.log(' ⏭️ Multi-vendor constraint already exists, skipping'); - return; - } - - // SQLite doesn't support ALTER CONSTRAINT, so we need to rebuild the table - console.log(' 📋 Rebuilding table with new constraint...'); - - // Create new table with correct schema - await run(db, ` - CREATE TABLE cves_new ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - cve_id VARCHAR(20) NOT NULL, - vendor VARCHAR(100) NOT NULL, - severity VARCHAR(20) NOT NULL, - description TEXT, - published_date DATE, - status VARCHAR(50) DEFAULT 'Open', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(cve_id, vendor) - ) - `); - - // Copy data - await run(db, ` - INSERT INTO cves_new (id, cve_id, vendor, severity, description, published_date, status, created_at, updated_at) - SELECT id, cve_id, vendor, severity, description, published_date, status, created_at, updated_at - FROM cves - `); - - // Drop old table - await run(db, 'DROP TABLE cves'); - - // Rename new table - await run(db, 'ALTER TABLE cves_new RENAME TO cves'); - - // Recreate indexes - await run(db, 'CREATE INDEX IF NOT EXISTS idx_cve_id ON cves(cve_id)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_vendor ON cves(vendor)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_severity ON cves(severity)'); - await run(db, 'CREATE INDEX IF NOT EXISTS idx_status ON cves(status)'); - - console.log(' ✓ Multi-vendor constraint applied'); -} - -async function createDefaultAdmin(db) { - console.log('5️⃣ Creating default admin user...'); - - const exists = await get(db, "SELECT id FROM users WHERE username = 'admin'"); - - if (exists) { - console.log(' ⏭️ Admin user already exists, skipping'); - return; - } - - const passwordHash = await bcrypt.hash('admin123', 10); - - await run(db, ` - INSERT INTO users (username, email, password_hash, role, is_active) - VALUES (?, ?, ?, ?, ?) - `, ['admin', 'admin@localhost', passwordHash, 'admin', 1]); - - console.log(' ✓ Admin user created (admin/admin123)'); -} - -async function updateView(db) { - console.log('6️⃣ Updating document status view...'); - - // Drop old view if exists - await run(db, 'DROP VIEW IF EXISTS cve_document_status'); - - // Create updated view with multi-vendor support - await run(db, ` - CREATE VIEW cve_document_status AS - SELECT - c.id as record_id, - c.cve_id, - c.vendor, - c.severity, - c.status, - COUNT(DISTINCT d.id) as total_documents, - COUNT(DISTINCT CASE WHEN d.type = 'advisory' THEN d.id END) as advisory_count, - COUNT(DISTINCT CASE WHEN d.type = 'email' THEN d.id END) as email_count, - COUNT(DISTINCT CASE WHEN d.type = 'screenshot' THEN d.id END) as screenshot_count, - CASE - WHEN COUNT(DISTINCT CASE WHEN d.type = 'advisory' THEN d.id END) > 0 - THEN 'Complete' - ELSE 'Missing Required Docs' - END as compliance_status - FROM cves c - LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor - GROUP BY c.id, c.cve_id, c.vendor, c.severity, c.status - `); - - console.log(' ✓ View updated'); -} - -// Run migration -migrate(); diff --git a/backend/migrate_jira_tickets.js b/backend/migrate_jira_tickets.js deleted file mode 100644 index dc66657..0000000 --- a/backend/migrate_jira_tickets.js +++ /dev/null @@ -1,39 +0,0 @@ -// Migration: Add jira_tickets table -const sqlite3 = require('sqlite3').verbose(); -const path = require('path'); - -const dbPath = path.join(__dirname, 'cve_database.db'); -const db = new sqlite3.Database(dbPath); - -console.log('Starting JIRA tickets migration...'); - -db.serialize(() => { - // Create jira_tickets table - db.run(` - CREATE TABLE IF NOT EXISTS jira_tickets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - cve_id TEXT NOT NULL, - vendor TEXT NOT NULL, - ticket_key TEXT NOT NULL, - url TEXT, - summary TEXT, - status TEXT DEFAULT 'Open' CHECK(status IN ('Open', 'In Progress', 'Closed')), - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (cve_id, vendor) REFERENCES cves(cve_id, vendor) ON DELETE CASCADE - ) - `, (err) => { - if (err) console.error('Error creating table:', err); - else console.log('✓ jira_tickets table created'); - }); - - // Create indexes - db.run('CREATE INDEX IF NOT EXISTS idx_jira_tickets_cve ON jira_tickets(cve_id, vendor)'); - db.run('CREATE INDEX IF NOT EXISTS idx_jira_tickets_status ON jira_tickets(status)'); - - console.log('✓ Indexes created'); -}); - -db.close(() => { - console.log('Migration complete!'); -}); diff --git a/backend/migrate_multivendor.js b/backend/migrate_multivendor.js deleted file mode 100644 index aa476d0..0000000 --- a/backend/migrate_multivendor.js +++ /dev/null @@ -1,128 +0,0 @@ -const sqlite3 = require('sqlite3').verbose(); -const db = new sqlite3.Database('./cve_database.db'); - -console.log('🔄 Starting database migration for multi-vendor support...\n'); - -db.serialize(() => { - // Backup existing data - console.log('📦 Creating backup tables...'); - db.run(`CREATE TABLE IF NOT EXISTS cves_backup AS SELECT * FROM cves`, (err) => { - if (err) console.error('Backup error:', err); - else console.log('✓ CVEs backed up'); - }); - - db.run(`CREATE TABLE IF NOT EXISTS documents_backup AS SELECT * FROM documents`, (err) => { - if (err) console.error('Backup error:', err); - else console.log('✓ Documents backed up'); - }); - - // Drop old table - console.log('\n🗑️ Dropping old cves table...'); - db.run(`DROP TABLE IF EXISTS cves`, (err) => { - if (err) { - console.error('Drop error:', err); - return; - } - console.log('✓ Old table dropped'); - - // Create new table with UNIQUE(cve_id, vendor) instead of UNIQUE(cve_id) - console.log('\n🏗️ Creating new cves table with multi-vendor support...'); - db.run(` - CREATE TABLE cves ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - cve_id VARCHAR(20) NOT NULL, - vendor VARCHAR(100) NOT NULL, - severity VARCHAR(20) NOT NULL, - description TEXT, - published_date DATE, - status VARCHAR(50) DEFAULT 'Open', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(cve_id, vendor) - ) - `, (err) => { - if (err) { - console.error('Create error:', err); - return; - } - console.log('✓ New table created with UNIQUE(cve_id, vendor)'); - - // Restore data - console.log('\n📥 Restoring data...'); - db.run(`INSERT INTO cves SELECT * FROM cves_backup`, (err) => { - if (err) { - console.error('Restore error:', err); - return; - } - console.log('✓ Data restored'); - - // Recreate indexes - console.log('\n🔍 Creating indexes...'); - db.run(`CREATE INDEX idx_cve_id ON cves(cve_id)`, () => { - console.log('✓ Index: idx_cve_id'); - }); - db.run(`CREATE INDEX idx_vendor ON cves(vendor)`, () => { - console.log('✓ Index: idx_vendor'); - }); - db.run(`CREATE INDEX idx_severity ON cves(severity)`, () => { - console.log('✓ Index: idx_severity'); - }); - db.run(`CREATE INDEX idx_status ON cves(status)`, () => { - console.log('✓ Index: idx_status'); - }); - - // Update view - console.log('\n👁️ Updating cve_document_status view...'); - db.run(`DROP VIEW IF EXISTS cve_document_status`, (err) => { - if (err) console.error('Drop view error:', err); - - db.run(` - CREATE VIEW cve_document_status AS - SELECT - c.id as record_id, - c.cve_id, - c.vendor, - c.severity, - c.status, - COUNT(DISTINCT d.id) as total_documents, - COUNT(DISTINCT CASE WHEN d.type = 'advisory' THEN d.id END) as advisory_count, - COUNT(DISTINCT CASE WHEN d.type = 'email' THEN d.id END) as email_count, - COUNT(DISTINCT CASE WHEN d.type = 'screenshot' THEN d.id END) as screenshot_count, - CASE - WHEN COUNT(DISTINCT CASE WHEN d.type = 'advisory' THEN d.id END) > 0 - THEN 'Complete' - ELSE 'Missing Required Docs' - END as compliance_status - FROM cves c - LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor - GROUP BY c.id, c.cve_id, c.vendor, c.severity, c.status - `, (err) => { - if (err) { - console.error('Create view error:', err); - } else { - console.log('✓ View recreated'); - } - - console.log('\n✅ Migration complete!'); - console.log('\n📊 Summary:'); - - db.get('SELECT COUNT(*) as count FROM cves', (err, row) => { - if (!err) console.log(` Total CVE entries: ${row.count}`); - - db.get('SELECT COUNT(DISTINCT cve_id) as count FROM cves', (err, row) => { - if (!err) console.log(` Unique CVE IDs: ${row.count}`); - - console.log('\n💡 Next steps:'); - console.log(' 1. Restart backend: pkill -f "node server.js" && node server.js &'); - console.log(' 2. Replace frontend/src/App.js with multi-vendor version'); - console.log(' 3. Test by adding same CVE with multiple vendors\n'); - - db.close(); - }); - }); - }); - }); - }); - }); - }); -}); diff --git a/docs/guides/full-reference-manual.md b/docs/guides/full-reference-manual.md new file mode 100644 index 0000000..1be14c6 --- /dev/null +++ b/docs/guides/full-reference-manual.md @@ -0,0 +1,1080 @@ +# STEAM Security Dashboard + +A self-hosted vulnerability management dashboard for the NTS-AEO-STEAM and NTS-AEO-ACCESS-ENG business units. Centralises CVE tracking, Ivanti host finding triage, AEO compliance posture, FP and Archer exception workflows, and internal documentation in a single interface. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Tech Stack](#tech-stack) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Configuration](#configuration) +- [Running the Application](#running-the-application) + - [Running as systemd services (auto-start on reboot)](#running-as-systemd-services-auto-start-on-reboot) +- [Features](#features) + - [Authentication and User Groups](#authentication-and-user-groups) + - [Home — CVE Management](#home--cve-management) + - [Reporting — Host Findings](#reporting--host-findings) + - [Ivanti Queue](#ivanti-queue) + - [Compliance — AEO Posture](#compliance--aeo-posture) + - [Knowledge Base](#knowledge-base) + - [Exports](#exports) + - [Jira Tickets](#jira-tickets) + - [Archer Risk Acceptance Tickets](#archer-risk-acceptance-tickets) + - [Admin Panel](#admin-panel) +- [Scripts](#scripts) +- [API Reference](#api-reference) +- [Architecture](#architecture) +- [Database Schema](#database-schema) +- [Security Model](#security-model) +- [Upgrading an Existing Deployment](#upgrading-an-existing-deployment) +- [Migrations](#migrations) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The STEAM Security Dashboard answers a common problem in vulnerability management: tracking which CVEs have been addressed, whether supporting vendor documentation exists, where each finding is in the remediation or exception workflow, and how the team's overall AEO compliance posture is trending week over week. + +The application provides: + +- A searchable, filterable CVE list with per-vendor tracking and document storage +- NVD API integration to auto-populate CVE metadata +- **Ivanti/RiskSense integration** — sync open host findings with live FP workflow tracking +- **Reporting page** with donut charts, advanced per-column filtering, inline editing, Ivanti Queue, and CSV/XLSX export +- **Ivanti Queue** — personal staging list for batch-processing FP, Archer, and CARD workflows +- **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 +- Group-based access control (Admin, Standard_User, Leadership, Read_Only) with a full audit trail + +--- + +## Tech Stack + +| Layer | Technology | +|---|---| +| Backend | Node.js 18+, Express 5 | +| Database | SQLite3 | +| File uploads | Multer 2 | +| Auth | bcryptjs, cookie-based sessions, express-rate-limit | +| Frontend | React 19, lucide-react, recharts, xlsx, react-markdown, rehype-sanitize, mermaid | +| Compliance xlsx parsing | Python 3, pandas, openpyxl | + +--- + +## Prerequisites + +- Node.js 18 or later +- npm +- Python 3 with `python3-pandas` and `python3-openpyxl` apt packages (required for compliance xlsx parsing) + +--- + +## Installation + +### 1. Clone the repository + +```bash +git clone +cd cve-dashboard +``` + +### 2. Install backend dependencies + +```bash +npm install +``` + +### 3. Install frontend dependencies + +```bash +cd frontend +npm install +``` + +### 4. Install Python dependencies + +Install via apt — this is the correct approach on Ubuntu/Debian and mirrors the dev server setup: + +```bash +apt install -y python3-pandas python3-openpyxl +``` + +> If apt packages are unavailable or you need a specific version, see `docs/python-venv-setup.md` for the venv fallback approach. + +> A bulk notes import script (`import_notes_from_csv.py`) is also available in `backend/scripts/` for maintenance tasks like backfilling notes from a CSV. It uses only Python stdlib. + +### 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 project root to create the SQLite database, all tables, indexes, triggers, and a default admin user: + +```bash +node backend/setup.js +``` + +This creates `backend/cve_database.db` with the complete v1.0.0 schema and generates a random admin password printed to stdout. **Save the password — it is only shown once.** + +> **Existing deployments:** If upgrading from a pre-v1.0.0 database, run the individual migration scripts in `backend/migrations/` instead. See `backend/migrations/README.md` for the full list and order. + +### 7. Build the frontend + +```bash +cd frontend +npm run build +``` + +Or use `npm start` for the development server (see [Running the Application](#running-the-application)). + +--- + +## Configuration + +The application is configured via `.env` files. These files are gitignored and must be created manually per environment. + +### Backend: `backend/.env` + +```env +PORT=3001 +API_HOST=localhost +CORS_ORIGINS=http://YOUR_IP:3000 +SESSION_SECRET= +# NODE_ENV=production — see note below + +# Optional: NVD API key for higher rate limits (50 req/30s vs 5 req/30s) +# Register at https://nvd.nist.gov/developers/request-an-api-key +NVD_API_KEY=your-key-here + +# Ivanti / RiskSense integration (required for Reporting page sync) +IVANTI_API_KEY=your-ivanti-api-key +IVANTI_CLIENT_ID=1550 +# Optional: filter workflows to a specific person's submissions +IVANTI_FIRST_NAME= +IVANTI_LAST_NAME= +# Set to 'true' if your network has SSL inspection / self-signed certs +IVANTI_SKIP_TLS=false + +# Jira Data Center REST API (required for Jira Tickets page) +# VPN or Charter Network connection required for all Jira instances. +# Service accounts use Basic Auth (JIRA_API_USER + JIRA_API_TOKEN). +# PATs require ATLSUP approval — set JIRA_AUTH_METHOD=pat to use JIRA_PAT instead. +# Rate limits: 1440 requests/day, burst of 60/minute. +JIRA_BASE_URL=https://jira.charter.com +JIRA_AUTH_METHOD=basic +JIRA_API_USER=your-service-account +JIRA_API_TOKEN=your-api-token +# JIRA_PAT=your-pat-token +JIRA_PROJECT_KEY=VULN +JIRA_ISSUE_TYPE=Task +JIRA_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`. + +**`NODE_ENV` and the Secure cookie flag:** When `NODE_ENV=production`, session cookies are set with the `Secure` flag, which means the browser will only send them over HTTPS connections. If you are running the application over plain HTTP (no TLS/SSL), you **must** leave `NODE_ENV` unset or set it to `development` — otherwise login will succeed but every subsequent API request will return 401 because the browser silently drops the cookie. Only set `NODE_ENV=production` when the application is served behind HTTPS (e.g., via a reverse proxy with TLS termination). + +### Frontend: `frontend/.env` + +```env +REACT_APP_API_BASE=http://YOUR_IP:3001/api +REACT_APP_API_HOST=http://YOUR_IP:3001 +``` + +Replace `YOUR_IP` with the machine's IP address or hostname. Use `localhost` for local-only access. + +> **Important:** React caches environment variables at build/start time. After changing `frontend/.env`, fully restart the frontend process — a browser refresh alone is not sufficient. + +--- + +## Running the Application + +### Using the helper scripts (recommended) + +From the project root: + +```bash +./start-servers.sh # Starts backend and frontend in the background +./stop-servers.sh # Stops all servers +``` + +The start script saves PIDs to `backend.pid` and `frontend.pid`. Logs are written to `backend/backend.log` and `frontend/frontend.log`. + +### Running manually + +```bash +# Terminal 1 — backend +cd backend +node server.js + +# Terminal 2 — frontend (development server) +cd frontend +npm start +``` + +### Running as systemd services (auto-start on reboot) + +Two systemd unit files are installed to `/etc/systemd/system/` so the dashboard starts automatically when the server boots: + +| Unit | What it runs | +|---|---| +| `cve-backend.service` | `node server.js` from `backend/` | +| `cve-frontend.service` | `npm start` from `frontend/` (waits for backend) | + +Both services load their respective `.env` files, restart on failure (5-second delay), and append output to `backend/backend.log` and `frontend/frontend.log`. + +**First-time setup** (if the units are not yet installed): + +```bash +# Copy the unit files into systemd +cp systemd/cve-backend.service /etc/systemd/system/ +cp systemd/cve-frontend.service /etc/systemd/system/ + +# Reload systemd, enable on boot, and start +systemctl daemon-reload +systemctl enable --now cve-backend cve-frontend +``` + +**Common commands:** + +```bash +systemctl status cve-backend cve-frontend # Check both services +systemctl restart cve-backend # Restart backend only +systemctl restart cve-frontend # Restart frontend only +systemctl stop cve-backend cve-frontend # Stop both +journalctl -u cve-backend -f # Follow backend journal +journalctl -u cve-frontend -f # Follow frontend journal +``` + +> The helper scripts (`start-servers.sh` / `stop-servers.sh`) still work for ad-hoc use, but systemd is the recommended approach for persistent deployments. If switching to systemd, stop any script-launched processes first with `./stop-servers.sh` to avoid port conflicts. + +### Default ports + +| Service | URL | +|---|---| +| Frontend | http://localhost:3000 | +| Backend API | http://localhost:3001 | + +--- + +## Features + +### Authentication and User Groups + +All routes require authentication. Four user groups are supported: + +| Group | Permissions | +|---|---| +| `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 | + +**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`. + +--- + +### Home — CVE Management + +The home page is the primary CVE research and tracking tool. + +**CVE List** +- Search CVEs by keyword (matches CVE ID, vendor, description) +- Filter by vendor, severity (Critical / High / Medium / Low), and status +- Color-coded severity badges: Critical (red), High (amber), Medium (sky blue), Low (green) +- Paginated list view + +**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 (ownership restrictions apply for Standard_User) +- The same CVE ID can be tracked across multiple vendors independently + +**Document Management** +- Upload documents attached to a CVE/vendor pair +- Supported document types: `advisory`, `email`, `screenshot`, `patch`, `other` +- Allowed file extensions: PDF, images (PNG, JPG, GIF, BMP, TIFF), Office documents (DOC, DOCX, XLS, XLSX, PPT, PPTX), text files (TXT, MD, CSV, LOG), email files (MSG, EML), and others (RTF, HTML, XML, JSON, YAML, ODF variants, ZIP, GZ, TAR, 7Z) +- File size limit: 10 MB per upload +- Admins can delete documents + +**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 (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 + +**Archer Ticket Quick Navigation** +- Archer EXC numbers shown on CVE rows +- Clicking an EXC badge navigates to the Reporting page pre-filtered to findings with that EXC number + +**Calendar Widget** +- Shows current month with red dot indicators on dates where Ivanti findings are due +- Click a date to navigate to the Reporting page filtered to that due date + +--- + +### Reporting — Host Findings + +The Reporting page is the core operational view for remediation tracking. It integrates with Ivanti/RiskSense to show all host findings for the configured business units. + +#### Syncing Data + +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.5–9.9 VRR) +2. Fetches the closed finding count separately +3. Sweeps closed findings to capture FP workflow states (including Approved FPs now closed) +4. Stores everything in the local SQLite cache + +Findings are also auto-synced on a 24-hour schedule. The last sync timestamp is shown at the top of the page. + +> `IVANTI_API_KEY` must be set in `backend/.env` for sync to work. + +#### Metric Charts + +| Chart | What it shows | +|---|---| +| **Open vs Closed** | Total open vs closed host findings direct from the Ivanti API | +| **Action Coverage** | Findings by action taken: FP Request · Archer Exception · Pending. Click a segment to filter the table. | +| **FP Finding Status** | How many *findings* are in each FP workflow state (Actionable, Requested, Reworked, Approved, Rejected, Expired) | +| **FP Workflow Status** | How many *unique FP# ticket IDs* are in each state — one ticket can cover many findings | + +#### Findings Table + +Each row represents a single Ivanti host finding. + +| Column | Description | +|---|---| +| Finding ID | Ivanti finding identifier | +| Severity | Numerical VRR score with group label (CRITICAL / HIGH) | +| Title | Vulnerability title | +| CVEs | Associated CVE IDs — up to 2 shown, remainder as "+N" badge | +| Host | Hostname — inline editable | +| IP Address | Host IP address | +| DNS | DNS/FQDN — inline editable | +| Due Date | Remediation due date; red if overdue, amber if within 30 days | +| SLA | SLA status: OVERDUE / AT_RISK / WITHIN_SLA | +| BU | Business unit | +| Workflow | FP# ticket ID and state badge — colour-coded by urgency | +| 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. Requires Admin or Standard_User group. + +**CVE Tooltips:** Hover over any CVE badge in the table to see a tooltip with the CVE description and severity (if the CVE exists in the local database). Tooltips appear after a 300ms delay, are cached in memory for the session, and auto-position to stay within the viewport. + +**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`. + +**Row visibility:** Hide individual rows by clicking the `EyeOff` icon on any row, or select multiple rows via checkboxes and click **Hide Selected** in the bulk action toolbar. Hidden rows are excluded from the table, the Action Coverage chart, and exports. Use the **Hidden (N)** button in the toolbar to view and restore hidden rows individually or all at once. Hidden row state persists to `localStorage` across sessions. Row hiding is a personal view preference available to all user groups. + +**Export:** Click **Export** to download the current filtered view as CSV or XLSX. Hidden rows and filtered rows are both excluded from exports. 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. 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") +- For **CARD** items: no vendor entry required — the IP address is captured automatically +- Select the workflow type: **FP**, **Archer**, or **CARD** +- Click **Add to Queue** — the row checkbox turns solid blue + +**Queue panel:** Click the **Queue** button (top right of Reporting page) to open the slide-out panel: +- **CARD** items appear at the top in their own section with the IP address displayed +- **FP and Archer** items are grouped alphabetically by vendor below +- Badges show workflow type: amber = FP, sky = Archer, green = CARD + +**Working the queue:** +- Check the green checkbox on an item to mark it complete (strikethrough at reduced opacity) +- Delete individual items with the trash icon, or select multiple and use **Delete (N)** +- **Clear Completed** removes all marked-complete items at once +- **Create FP Workflow** — select pending FP items and click to open the FP Workflow modal, which submits a False Positive workflow batch directly to the Ivanti API with form fields, file attachments, and scope override. Attachments can be local file uploads or documents selected from the CVE document library — library documents are read from disk and sent to Ivanti identically to local uploads. Successful submission marks the queue items as complete and records the submission locally. + +**Redirecting completed items:** +- Completed items show a redirect button (↱) next to the delete icon +- Click redirect to open a modal where you select the target workflow type (FP, Archer, or CARD) and vendor (required for FP/Archer) +- Redirecting creates a new pending queue item with the same finding data under the new workflow type — the original completed item is preserved +- This is useful when a CARD inventory fix is done but the finding still needs an FP or Archer workflow, or when an item was assigned to the wrong workflow initially +- Not every completed item needs a redirect — it's an optional action for items that require further processing + +Queue items are stored in the database, are **personal to your login**, and persist across sessions and page refreshes. + +--- + +### Compliance — AEO Posture + +The Compliance page tracks NTS-AEO team posture against the AEO compliance framework using weekly xlsx reports exported from the NTS_AEO reporting system. + +#### Upload Workflow + +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 backend extracts the xlsx schema and runs a **drift check** against the parser configuration (`compliance_config.json`). If structural drift is detected, a drift review phase is shown before the diff preview: + - **Breaking** findings (red) — missing core columns or detail sheets — block the upload until the config is updated + - **Silent-miss** findings (amber) — unknown metrics or sheets that will be miscategorised — warn but allow proceeding + - **Cosmetic** findings (muted) — new columns or stale config entries — informational only + - Admins can click **Reconcile Config** to auto-patch the parser configuration and re-run the check +3. If no breaking drift exists, the **diff preview** is shown — new violations, resolved items, and recurring items since the last upload +4. Click **Confirm Upload** to commit. The upload is recorded and the device table updates immediately. + +The report date is extracted automatically from the filename. + +**Upload rollback:** Admins can roll back the most recent upload via `POST /api/compliance/rollback/:uploadId`. Rolling back deletes new items introduced by that upload, re-activates items it resolved, and decrements seen counts on recurring items. + +#### Metric Health Cards + +Each AEO metric (e.g., `2.3.4i`, `5.2.4`) is shown as a health card displaying: +- Compliance percentage vs target +- Status: Meets/Exceeds Target · Within 15% of Target · Below 15% of Target + +Click a card to filter the device table to only devices failing that metric. + +#### Device Table + +Shows all devices currently failing one or more metrics (Active tab) or previously resolved (Resolved tab). Columns: Hostname, IP Address, Type, Failing Metrics, Times Seen. Click a row to open the detail panel. + +#### Detail Panel + +A slide-out panel for a selected device showing: +- **Failing Metrics** — each metric with surfaced extra fields (CVEs, SLA status, due date, OS, EoL, Splunk last seen, MFA software) +- 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 can be deleted by the author or an Admin — deleting a multi-metric note removes it from all linked metrics. Requires Admin or Standard_User group. + +Notes persist across uploads and are keyed to the device hostname and metric ID. + +#### Teams + +Only **STEAM** and **ACCESS-ENG** teams are tracked. The team selector at the top of the page switches context between them. + +--- + +### Knowledge Base + +A document library for internal reference material — policies, runbooks, vendor advisories, and process guides. + +- 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 +- 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). + +--- + +### Exports + +Bulk export tools for reports and data extracts. Available to Admin, Standard_User, and Leadership groups. Read_Only users cannot access the Exports page. + +--- + +### Jira Tickets + +A dedicated page for managing Jira Data Center tickets linked to CVE/vendor pairs. Accessible from the navigation drawer. Requires a configured Jira API connection (see [Configuration](#configuration)). + +**Ticket list** +- View all tracked Jira tickets with status, CVE ID, vendor, summary, and Jira key +- Filter by status or search by keyword +- Click a Jira key to open the issue in Jira Data Center + +**Jira API operations (Admin/Standard_User)** +- **Lookup** — search for any Jira issue by key and view its current status, assignee, and summary +- **Create in Jira** — create a new Jira issue directly from the dashboard with project key, issue type, summary, and description; the resulting ticket is automatically linked to a CVE/vendor pair in the local database +- **Sync** — refresh a single ticket's status and summary from Jira, or bulk-sync all tracked tickets via JQL search +- **Create / Edit / Delete** — manage local ticket records linking Jira keys to CVE/vendor pairs + +**Connection test (Admin)** — verify Jira API credentials and connectivity from the page header. + +**Rate limit monitoring (Admin)** — view current burst and daily rate limit usage against Charter's posted limits (60/minute burst, 1 440/day). + +All Jira API calls are proxied through the backend. Credentials are never exposed to the browser. Rate limits are enforced client-side with inter-request delays (1s for GETs, 2s for writes). See `docs/jira-api-use-cases.md` for the full API compliance summary. + +--- + +### Archer Risk Acceptance Tickets + +Track Archer exception tickets (EXC numbers) linked to specific CVE/vendor pairs. + +- EXC number format: `EXC-NNNNN` +- Statuses: `Draft`, `Open`, `Under Review`, `Accepted` +- 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) + +--- + +### Admin Panel + +The Admin Panel is a full-page, tabbed interface accessible only to Admin-group users. It replaces the previous inline modal rendering and follows the dashboard's dark tactical intelligence theme. Three tabs provide consolidated access to administrative functions: + +**User Management** — the default tab. Displays a themed user table with group badges (Admin in red, Standard_User in accent blue, Leadership in amber, Read_Only in muted grey). Admins can create, edit, and delete users, change group assignments, and toggle active status — all through inline forms styled to match the dashboard. Admins cannot demote themselves or deactivate their own account. Deactivating a user immediately invalidates all their active sessions. All group changes are audit-logged with previous and new group values. + +**Audit Log** — a paginated, filterable log table showing every state-changing action with timestamp, username, action type, entity type, entity ID, details, and IP address. Action types are colour-coded: login in green, delete in red, create in accent blue, update in amber. Filter by username, action type, entity type, and date range. Results are paginated at 25 per page. + +**System Info** — stat cards showing total user count, active user count, total audit log entries, and users who logged in within the last 7 days. A "Recent Activity" section lists the 10 most recent audit log entries. + +The `UserMenu` quick-access links ("Manage Users", "Audit Log") continue to open the existing modal components for fast access without navigating to the admin page. + +--- + +## Scripts + +### `backend/scripts/parse_compliance_xlsx.py` + +Called automatically by the compliance upload flow. Parses the NTS_AEO xlsx report and outputs structured JSON to stdout for consumption by the Node compliance route. Reads metric categories, core columns, and skip sheets from `compliance_config.json` (shared with the drift checker). + +- Reads all detail sheets; skips sheets listed in `skip_sheets` +- Filters to rows where `Compliant == False` +- Extracts hostname, IP, device type, team, and metric ID per row +- Captures all non-core columns in `extra_json` (CVEs, SLA status, OS, EoL, Splunk, MFA, Ivanti_Vulnerability_ID, etc.) +- Parses `Summary` sheet for per-team metric health (compliance_pct, target, status) +- Extracts report date from the filename (`NTS_AEO_YYYY_MM_DD.xlsx`) + +**Dependencies:** `pandas>=2.0.0`, `openpyxl>=3.0.0` + +### `backend/scripts/extract_xlsx_schema.py` + +Called by the preview endpoint before parsing. Extracts the structural schema of an xlsx file as JSON — sheet names, first-row column headers per sheet, and unique metric values from the Summary sheet. The Node.js drift checker compares this schema against `compliance_config.json` to detect breaking, silent-miss, and cosmetic drift. + +**Dependencies:** `openpyxl>=3.0.0` + +### `backend/scripts/compliance_config.json` + +Shared parser configuration file — the single source of truth for `metric_categories` (metric ID → category mapping), `core_cols` (columns that become main item fields), and `skip_sheets` (sheets excluded from parsing). Read by both `parse_compliance_xlsx.py` and the Node.js `driftChecker.js` module. Admins can auto-patch this file via the **Reconcile Config** button in the upload modal. + +--- + +## API Reference + +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 | Group | Description | +|---|---|---|---| +| 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` | Any | Get current user info (returns `group` field) | +| POST | `/api/auth/cleanup-sessions` | Admin | Delete expired sessions | + +### CVEs + +| Method | Path | Group | Description | +|---|---|---|---| +| 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 | +| GET | `/api/cves/:cveId/tooltip` | Any | Get CVE description and severity for tooltip display (truncated to 300 chars) | + +### Documents + +| Method | Path | Group | Description | +|---|---|---|---| +| 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 | Group | Description | +|---|---|---|---| +| 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) | +| GET | `/api/jira-tickets/connection-test` | Admin | Test Jira API connectivity and credentials | +| GET | `/api/jira-tickets/rate-limit` | Admin | Get current burst and daily rate limit usage | +| GET | `/api/jira-tickets/lookup/:issueKey` | Any | Look up a single Jira issue by key | +| POST | `/api/jira-tickets/search` | Any | JQL search for Jira issues | +| POST | `/api/jira-tickets/create-in-jira` | Admin, Standard_User | Create an issue in Jira and link it locally | +| POST | `/api/jira-tickets/sync-all` | Admin | Bulk-sync all tracked tickets via JQL | +| POST | `/api/jira-tickets/:id/sync` | Admin, Standard_User | Sync a single ticket's status from Jira | + +### Ivanti — Host Findings + +| Method | Path | Group | Description | +|---|---|---|---| +| 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 | Group | Description | +|---|---|---|---| +| GET | `/api/ivanti/workflows` | Any | Get cached workflow data | +| POST | `/api/ivanti/workflows/sync` | Admin, Standard_User | Trigger an immediate workflow sync | + +### Ivanti — FP Workflow Submission + +| Method | Path | Group | Description | +|---|---|---|---| +| GET | `/api/ivanti/fp-workflow/documents/search` | Any | Search the CVE document library by name, CVE ID, or vendor; returns up to 50 matches | +| POST | `/api/ivanti/fp-workflow` | Admin, Standard_User | Submit an FP workflow batch to Ivanti API (multipart/form-data with local attachments and/or `libraryDocIds`) | +| GET | `/api/ivanti/fp-workflow/submissions` | Any | List FP submissions for the current user | +| PUT | `/api/ivanti/fp-workflow/submissions/:id` | Admin, Standard_User | Update an FP submission (edit form fields) | +| POST | `/api/ivanti/fp-workflow/submissions/:id/findings` | Admin, Standard_User | Add or remove findings on an existing submission | +| POST | `/api/ivanti/fp-workflow/submissions/:id/attachments` | Admin, Standard_User | Upload additional attachments (local files and/or `libraryDocIds`) to an existing submission | +| PATCH | `/api/ivanti/fp-workflow/submissions/:id/status` | Admin, Standard_User | Update submission lifecycle status | + +### Ivanti — Todo Queue + +| Method | Path | Group | Description | +|---|---|---|---| +| 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 | +| POST | `/api/ivanti/todo-queue/batch` | Admin, Standard_User | Batch-add multiple findings to the queue | +| PUT | `/api/ivanti/todo-queue/:id` | Admin, Standard_User | Update a queue item (mark complete, edit vendor/type) | +| POST | `/api/ivanti/todo-queue/:id/redirect` | Admin, Standard_User | Redirect a completed item to a different workflow 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 | Group | Description | +|---|---|---|---| +| POST | `/api/compliance/preview` | Admin, Standard_User | Parse an xlsx upload, run drift check, and return drift report + diff + temp file path | +| POST | `/api/compliance/commit` | Admin, Standard_User | Commit a previewed upload to the database | +| POST | `/api/compliance/reconcile-config` | Admin | Auto-patch `compliance_config.json` to resolve breaking and silent-miss drift findings | +| POST | `/api/compliance/rollback/:uploadId` | Admin | Roll back the most recent upload (deletes new items, re-activates resolved items) | +| 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; accepts `metric_ids` array for multi-metric notes | +| DELETE | `/api/compliance/notes/:id` | Admin, Standard_User | Delete a note by ID; `?group=true` deletes all notes sharing the same `group_id`. Author or Admin only. | + +### Knowledge Base + +| Method | Path | Group | Description | +|---|---|---|---| +| 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 | Group | Description | +|---|---|---|---| +| 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 | 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 | + +### Audit Logs (Admin only) + +| 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 | + +### Utility + +| Method | Path | Group | Description | +|---|---|---|---| +| GET | `/api/vendors` | Any | List all distinct vendor names | +| GET | `/api/stats` | Any | Dashboard statistics | + +--- + +## Architecture + +``` +cve-dashboard/ +├── start-servers.sh # Start backend + frontend in background +├── stop-servers.sh # Stop all servers +├── package.json # Root package.json (backend dependencies) +├── systemd/ # systemd unit files for auto-start on boot +│ ├── cve-backend.service +│ └── cve-frontend.service +│ +├── backend/ +│ ├── server.js # Express app — routes, middleware, security headers +│ ├── setup.js # One-time DB initialization and default admin creation +│ ├── cve_database.db # SQLite database (gitignored) +│ ├── uploads/ # File storage root (gitignored) +│ │ ├── // # CVE documents +│ │ ├── knowledge_base/ # Knowledge base documents +│ │ └── temp/ # Temporary upload staging +│ ├── routes/ +│ │ ├── auth.js # Login, logout, session check, rate limiting +│ │ ├── users.js # User CRUD (admin) +│ │ ├── auditLog.js # Audit log viewer (admin) +│ │ ├── nvdLookup.js # NVD API proxy +│ │ ├── knowledgeBase.js # Knowledge base document management +│ │ ├── archerTickets.js # Archer EXC ticket CRUD +│ │ ├── 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 +│ │ ├── jiraTickets.js # Jira ticket CRUD + Jira REST API integration (lookup, sync, create) +│ │ └── compliance.js # AEO compliance upload, diff, device tracking, notes +│ ├── middleware/ +│ │ └── auth.js # requireAuth and requireGroup middleware +│ ├── helpers/ +│ │ ├── auditLog.js # logAudit helper (fire-and-forget) +│ │ ├── driftChecker.js # Schema drift detection: compareSchemaToDrift(), loadConfig(), reconcileConfig() +│ │ ├── ivantiApi.js # Ivanti API HTTP helpers (multipart, JSON, form POST) +│ │ └── jiraApi.js # Jira Data Center REST API helpers (Basic/PAT auth, rate limiting) +│ ├── migrations/ # Sequential migration scripts (run manually with node) +│ └── scripts/ +│ ├── compliance_config.json # Shared parser config (metric_categories, core_cols, skip_sheets) +│ ├── extract_xlsx_schema.py # Extracts xlsx structure as JSON for drift checking +│ ├── parse_compliance_xlsx.py # Parses NTS_AEO xlsx compliance reports +│ ├── import_notes_from_csv.py # Bulk-import finding notes from CSV +│ └── requirements.txt # pandas, openpyxl +│ +└── frontend/ + └── src/ + ├── App.js # Home dashboard — CVE list, filters, modals, calendar + ├── App.css # Global styles and CSS variables + ├── contexts/ + │ └── AuthContext.js # Auth state provider (login, logout, group helpers) + └── components/ + ├── LoginForm.js # Login page + ├── NavDrawer.js # Side navigation drawer (pages + 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 modal (quick-access from UserMenu) + ├── AuditLog.js # Admin audit log modal (quick-access from UserMenu) + ├── NvdSyncModal.js # Bulk NVD sync dialog + ├── KnowledgeBaseModal.js # Knowledge base upload/list modal + ├── KnowledgeBaseViewer.js # Inline document viewer (sandboxed iframe, sanitized markdown) + ├── ConfirmModal.js # Themed confirmation dialog (replaces window.confirm) + ├── CveTooltip.js # Hover tooltip for CVE badges (portal-rendered, cached) + ├── RedirectModal.js # Queue item redirect modal (workflow type + vendor selection) + └── pages/ + ├── AdminPage.js # Admin panel: user management, audit log, system info + ├── 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 + ├── JiraPage.js # Jira ticket management and Jira API integration + ├── KnowledgeBasePage.js # Knowledge base page + └── ExportsPage.js # Exports page (group-gated) +``` + +--- + +## Database Schema + +### Core tables (created by `setup.js`) + +**`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)` with `ON DELETE CASCADE`. + +**`required_documents`** — Vendor-specific document requirements. + +**`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. + +**`audit_logs`** — Append-only log of all state-changing actions. + +### Feature tables (added by migrations) + +**`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)`. 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. + +**`ivanti_findings_cache`** — Single-row cache for Ivanti host findings. + +**`ivanti_finding_notes`** — Persistent per-finding notes keyed by finding ID. Survives cache refreshes. `UNIQUE(finding_id)`. + +**`ivanti_counts_cache`** — Single-row cache for finding metrics: open/closed counts, FP workflow state breakdowns by finding and by unique ticket ID. + +**`ivanti_finding_overrides`** — Editor-applied overrides for `hostName` and `dns` fields. `UNIQUE(finding_id, field)`. + +**`ivanti_todo_queue`** — Personal per-user queue of findings staged for FP, Archer, or CARD processing. Keyed by `(user_id, finding_id)`. Completed items can be redirected to a different workflow type via `POST /:id/redirect`, which creates a new pending item preserving the original finding data. + +**`ivanti_fp_submissions`** — Record of FP workflow submissions to the Ivanti API. Tracks user, workflow batch ID, form fields, finding IDs, queue item IDs, attachment results, and submission status (success/partial/failed). + +**`compliance_uploads`** — Record of each compliance xlsx upload: filename, report date, uploader, timestamp, and new/resolved/recurring counts. + +**`compliance_items`** — One row per device/metric violation. Tracks hostname, IP, device type, team, metric ID, category, `extra_json` (all non-core xlsx columns), status (active/resolved), first seen upload, and times seen. Identity key: `(hostname, metric_id)`. + +**`compliance_notes`** — Timestamped notes per hostname/metric. Multiple notes per combination are supported. `group_id` column links notes created in the same multi-metric submission. Foreign-key linked to compliance items. + +### View + +**`cve_document_status`** — Aggregates document counts per CVE/vendor and derives a `compliance_status` (`Complete` when an advisory is present, otherwise `Missing Required Docs`). + +--- + +## 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,}$/` +- Severity must be one of: `Critical`, `High`, `Medium`, `Low` +- 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 + +### Security headers + +Applied to all responses: + +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: SAMEORIGIN` +- `X-XSS-Protection: 1; mode=block` +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Permissions-Policy: camera=(), microphone=(), geolocation=()` + +--- + +## Upgrading an Existing Deployment + +This procedure updates the application code and schema while preserving all existing data. The database file (`backend/cve_database.db`) is never overwritten by `git pull` — it is gitignored. + +```bash +# 1. Stop the running servers +cd /home/cve-dashboard +./stop-servers.sh + +# 2. Pull latest code +git pull origin master + +# 3. Install backend dependencies (picks up any new packages) +npm install + +# 4. Install frontend dependencies +cd frontend +npm install +cd .. + +# 5. Ensure SESSION_SECRET is set in backend/.env +# If missing: +# echo "SESSION_SECRET=$(openssl rand -base64 32)" >> backend/.env + +# 6. Run all migrations (idempotent — safe to re-run, skips already-applied changes) +cd backend +node migrations/add_knowledge_base_table.js +node migrations/add_archer_tickets_table.js +node migrations/add_ivanti_sync_table.js +node migrations/add_ivanti_findings_tables.js +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_todo_queue_hostname.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_fp_submissions_table.js +node migrations/add_user_groups.js +node migrations/add_created_by_columns.js +node migrations/add_fp_submission_editing.js +node migrations/add_granite_workflow_type.js +node migrations/add_compliance_notes_group_id.js +cd .. + +# 7. Rebuild the frontend +cd frontend +npm run build +cd .. + +# 8. Start servers +./start-servers.sh +# Or, if using systemd services: +# systemctl restart cve-backend cve-frontend +``` + +After upgrading, clear your browser cookies and log in fresh — session format changes between versions will invalidate old sessions. + +> **Do not re-run `node setup.js`** on an existing deployment. It is only for first-time initialization. Re-running it will not destroy data (it checks for existing tables/users), but it is unnecessary and may create a duplicate admin account. + +> **NODE_ENV reminder:** If you are running over plain HTTP (no TLS), make sure `NODE_ENV` is **not** set to `production` in `backend/.env`. See [Troubleshooting](#troubleshooting) for details. + +--- + +## Migrations + +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 +node migrations/add_knowledge_base_table.js +node migrations/add_archer_tickets_table.js +node migrations/add_ivanti_sync_table.js +node migrations/add_ivanti_findings_tables.js +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_todo_queue_hostname.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_fp_submissions_table.js +node migrations/add_user_groups.js +node migrations/add_created_by_columns.js +node migrations/add_fp_submission_editing.js +node migrations/add_granite_workflow_type.js +node migrations/add_compliance_notes_group_id.js +``` + +For deployments upgrading from an older schema, the following legacy migration scripts are also available in `backend/`: + +- `migrate_multivendor.js` — Adds multi-vendor support to an older single-vendor schema +- `migrate-audit-log.js` — Adds the `audit_logs` table to pre-auth deployments +- `migrate-to-1.1.js` — General 1.0 → 1.1 schema update + +> Several columns (`fp_workflow_counts_json`, `fp_id_counts_json`, `seen_count`, `summary_json`) are added automatically via idempotent `ALTER TABLE` statements each time the server starts. No manual re-run is needed. + +--- + +## Troubleshooting + +### Login succeeds but all pages show "Error Loading" / 401 Unauthorized + +**Symptom:** You can log in successfully, but the dashboard shows "Error Loading CVEs", "Failed to fetch", and the browser console shows 401 on every API call. + +**Cause:** The session cookie has the `Secure` flag set (because `NODE_ENV=production` in `backend/.env`), but the application is being accessed over plain HTTP. Browsers silently refuse to send `Secure` cookies over non-HTTPS connections, so every request after login arrives without a session cookie. + +**Fix:** Either: +1. Remove `NODE_ENV=production` from `backend/.env` (or set it to `development`) and restart the backend, **or** +2. Set up HTTPS (e.g., via nginx reverse proxy with TLS termination) and access the app over `https://` + +### Login fails with "Too many login attempts" + +**Cause:** The login endpoint is rate-limited to 20 attempts per 15-minute window. Wait 15 minutes or restart the backend to reset the counter. + +### Server refuses to start: "SESSION_SECRET environment variable must be set" + +**Fix:** Add a `SESSION_SECRET` to `backend/.env`: +```bash +echo "SESSION_SECRET=$(openssl rand -base64 32)" >> backend/.env +``` + +### After upgrading: "user_group" errors or missing group data + +**Fix:** Run the group migration: +```bash +cd backend +node migrations/add_user_groups.js +node migrations/add_created_by_columns.js +``` +This maps existing roles to groups automatically (admin→Admin, editor→Standard_User, viewer→Read_Only). diff --git a/frontend/src/App.js.v1 b/frontend/src/App.js.v1 deleted file mode 100644 index 16769f6..0000000 --- a/frontend/src/App.js.v1 +++ /dev/null @@ -1,598 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader } from 'lucide-react'; - -const API_BASE = 'http://192.168.2.117:3001/api'; - -const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low']; - -export default function App() { - const [searchQuery, setSearchQuery] = useState(''); - const [selectedVendor, setSelectedVendor] = useState('All Vendors'); - const [selectedSeverity, setSelectedSeverity] = useState('All Severities'); - const [selectedCVE, setSelectedCVE] = useState(null); - const [selectedDocuments, setSelectedDocuments] = useState([]); - const [cves, setCves] = useState([]); - const [vendors, setVendors] = useState(['All Vendors']); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [cveDocuments, setCveDocuments] = useState({}); - const [quickCheckCVE, setQuickCheckCVE] = useState(''); - const [quickCheckResult, setQuickCheckResult] = useState(null); - const [showAddCVE, setShowAddCVE] = useState(false); - const [newCVE, setNewCVE] = useState({ - cve_id: '', - vendor: '', - severity: 'Medium', - description: '', - published_date: new Date().toISOString().split('T')[0] - }); - const [uploadingFile, setUploadingFile] = useState(false); - - // Fetch CVEs from API - useEffect(() => { - fetchCVEs(); - fetchVendors(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Refetch when filters change - useEffect(() => { - fetchCVEs(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [searchQuery, selectedVendor, selectedSeverity]); - - const fetchCVEs = async () => { - setLoading(true); - setError(null); - try { - const params = new URLSearchParams(); - if (searchQuery) params.append('search', searchQuery); - if (selectedVendor !== 'All Vendors') params.append('vendor', selectedVendor); - if (selectedSeverity !== 'All Severities') params.append('severity', selectedSeverity); - - const response = await fetch(`${API_BASE}/cves?${params}`); - if (!response.ok) throw new Error('Failed to fetch CVEs'); - const data = await response.json(); - setCves(data); - } catch (err) { - setError(err.message); - console.error('Error fetching CVEs:', err); - } finally { - setLoading(false); - } - }; - - const fetchVendors = async () => { - try { - const response = await fetch(`${API_BASE}/vendors`); - if (!response.ok) throw new Error('Failed to fetch vendors'); - const data = await response.json(); - setVendors(['All Vendors', ...data]); - } catch (err) { - console.error('Error fetching vendors:', err); - } - }; - - const fetchDocuments = async (cveId) => { - if (cveDocuments[cveId]) return; - - try { - const response = await fetch(`${API_BASE}/cves/${cveId}/documents`); - if (!response.ok) throw new Error('Failed to fetch documents'); - const data = await response.json(); - setCveDocuments(prev => ({ ...prev, [cveId]: data })); - } catch (err) { - console.error('Error fetching documents:', err); - } - }; - - const quickCheckCVEStatus = async () => { - if (!quickCheckCVE.trim()) return; - - try { - const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`); - if (!response.ok) throw new Error('Failed to check CVE'); - const data = await response.json(); - setQuickCheckResult(data); - } catch (err) { - console.error('Error checking CVE:', err); - setQuickCheckResult({ error: err.message }); - } - }; - - const handleViewDocuments = async (cveId) => { - if (selectedCVE === cveId) { - setSelectedCVE(null); - } else { - setSelectedCVE(cveId); - await fetchDocuments(cveId); - } - }; - - const getSeverityColor = (severity) => { - const colors = { - 'Critical': 'bg-red-100 text-red-800', - 'High': 'bg-orange-100 text-orange-800', - 'Medium': 'bg-yellow-100 text-yellow-800', - 'Low': 'bg-blue-100 text-blue-800' - }; - return colors[severity] || 'bg-gray-100 text-gray-800'; - }; - - const toggleDocumentSelection = (docId) => { - setSelectedDocuments(prev => - prev.includes(docId) - ? prev.filter(id => id !== docId) - : [...prev, docId] - ); - }; - - const exportSelectedDocuments = () => { - alert(`Exporting ${selectedDocuments.length} documents for report attachment`); - }; - - const handleAddCVE = async (e) => { - e.preventDefault(); - try { - const response = await fetch(`${API_BASE}/cves`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(newCVE) - }); - - if (!response.ok) throw new Error('Failed to add CVE'); - - alert(`CVE ${newCVE.cve_id} added successfully!`); - setShowAddCVE(false); - setNewCVE({ - cve_id: '', - vendor: '', - severity: 'Medium', - description: '', - published_date: new Date().toISOString().split('T')[0] - }); - fetchCVEs(); - } catch (err) { - alert(`Error: ${err.message}`); - } - }; - - const handleFileUpload = async (cveId, vendor) => { - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = '.pdf,.png,.jpg,.jpeg,.txt,.doc,.docx'; - - fileInput.onchange = async (e) => { - const file = e.target.files[0]; - if (!file) return; - - const docType = prompt( - 'Document type (advisory, email, screenshot, patch, other):', - 'advisory' - ); - if (!docType) return; - - const notes = prompt('Notes (optional):'); - - setUploadingFile(true); - - const formData = new FormData(); - formData.append('file', file); - formData.append('cveId', cveId); - formData.append('vendor', vendor); - formData.append('type', docType); - if (notes) formData.append('notes', notes); - - try { - const response = await fetch(`${API_BASE}/cves/${cveId}/documents`, { - method: 'POST', - body: formData - }); - - if (!response.ok) throw new Error('Failed to upload document'); - - alert(`Document uploaded successfully!`); - delete cveDocuments[cveId]; - await fetchDocuments(cveId); - fetchCVEs(); - } catch (err) { - alert(`Error: ${err.message}`); - } finally { - setUploadingFile(false); - } - }; - - fileInput.click(); - }; - - const filteredCVEs = cves; - - return ( -
-
- {/* Header */} -
-
-

CVE Dashboard

-

Query vulnerabilities, manage vendors, and attach documentation

-
- -
- - {/* Add CVE Modal */} - {showAddCVE && ( -
-
-
-
-

Add New CVE

- -
- -
-
- - setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()})} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" - /> -
- -
- - setNewCVE({...newCVE, vendor: e.target.value})} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" - /> -
- -
- - -
- -
- -