diff --git a/.kiro/steering/tech.md b/.kiro/steering/tech.md index 23eb5c8..eb78755 100644 --- a/.kiro/steering/tech.md +++ b/.kiro/steering/tech.md @@ -75,11 +75,36 @@ Python dependencies: `pandas>=2.0.0`, `openpyxl>=3.0.0` (install via apt or venv ## Environment Configuration -- `backend/.env` — PORT, CORS_ORIGINS, SESSION_SECRET, NVD_API_KEY, Ivanti API credentials +- `backend/.env` — PORT, CORS_ORIGINS, SESSION_SECRET, NVD_API_KEY, Ivanti API credentials, CARD API credentials - `frontend/.env` — REACT_APP_API_BASE, REACT_APP_API_HOST - Both `.env` files are gitignored; see `.env.example` files for templates. - React env vars are baked in at **build time** — you must rebuild (`npm run build`) after changing them. +### Key Backend Env Vars + +| Variable | Purpose | +|---|---| +| `IVANTI_API_KEY` | RiskSense platform API key | +| `IVANTI_CLIENT_ID` | RiskSense client ID (default: 1550) | +| `IVANTI_BU_FILTER` | Comma-separated BU teams to sync findings for (default: `NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM`) | +| `IVANTI_FIRST_NAME` / `IVANTI_LAST_NAME` | Fallback Ivanti identity for workflow sync (used only if no per-user identities configured) | +| `CARD_API_URL` | CARD API base URL (e.g., `https://card.charter.com`) | +| `CARD_API_USER` / `CARD_API_PASS` | CARD OAuth credentials for Bearer token acquisition | +| `CARD_SKIP_TLS` | Set to `true` to skip TLS verification (for SSL inspection proxies) | +| `DATABASE_URL` | PostgreSQL connection string | + +### CARD API and Ivanti Integration Details + +See `.kiro/steering/integrations.md` for full API contracts, response shapes, and quirks for CARD, Ivanti, Atlas, and Jira. + +### Ivanti Findings IPv6 Handling + +Some Ivanti findings have no IPv4 address. The sync captures fallback addresses: +- `qualys_ipv6` — from `hostAdditionalDetails[].["IPv6 Address"]` (resolves in CARD) +- `primary_ipv6` — from `assetCustomAttributes['1550_host_6'][0]` (may not resolve in CARD) + +Display priority in the UI: IPv4 > Qualys IPv6 (amber "Q" badge) > Primary IPv6 (indigo "v6" badge) + ## Code Style & Lint Rules ### Unused Variables diff --git a/backend/routes/ivantiFindings.js b/backend/routes/ivantiFindings.js index c9afe79..6d75c5e 100644 --- a/backend/routes/ivantiFindings.js +++ b/backend/routes/ivantiFindings.js @@ -681,7 +681,7 @@ async function syncFPWorkflowCounts(openFindings, apiKey, clientId, skipTls) { const MANAGED_BUS_VALUE = process.env.IVANTI_MANAGED_BUS || 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM'; const EXPECTED_BUS = new Set(MANAGED_BUS_VALUE.split(',').map(b => b.trim()).filter(Boolean)); -async function runBUDriftChecker(newlyArchivedIds, apiKey, clientId, skipTls) { +async function runBUDriftChecker(newlyArchivedIds, apiKey, clientId, skipTls, previousBuMap) { const summary = { bu_reassignment: 0, severity_drift: 0, closed_on_platform: 0, decommissioned: 0 }; if (!newlyArchivedIds || newlyArchivedIds.length === 0) return summary; @@ -797,12 +797,8 @@ async function runBUDriftChecker(newlyArchivedIds, apiKey, clientId, skipTls) { // Record BU reassignment in ivanti_finding_bu_history for detail view if (classification === 'bu_reassignment' && found) { try { - // Determine previous BU — look up from the cached finding record - const { rows: prevRows } = await pool.query( - `SELECT bu_ownership FROM ivanti_findings WHERE id = $1`, - [id] - ); - const previousBu = prevRows[0]?.bu_ownership || 'UNKNOWN'; + // Determine previous BU from the pre-sync snapshot (passed in from syncFindings) + const previousBu = (previousBuMap && previousBuMap.get(id)) || 'UNKNOWN'; await pool.query( `INSERT INTO ivanti_finding_bu_history (finding_id, finding_title, host_name, previous_bu, new_bu, detected_at) VALUES ($1, $2, $3, $4, $5, NOW())`, @@ -897,12 +893,14 @@ async function syncFindings() { // Read previous open findings from DB for archive detection let previousFindings = []; + let previousBuMap = new Map(); // id → bu_ownership snapshot BEFORE upsert try { const { rows } = await pool.query( `SELECT id, title, host_name AS "hostName", ip_address AS "ipAddress", severity, bu_ownership AS "buOwnership" FROM ivanti_findings WHERE state = 'open'` ); previousFindings = rows; + previousBuMap = new Map(rows.map(f => [String(f.id), f.buOwnership || ''])); } catch (err) { console.error('[Ivanti Findings] Failed to read previous findings for archive detection:', err.message); } @@ -1004,7 +1002,7 @@ async function syncFindings() { console.log(`[BU Drift Checker] ${idsToCheck.length} disappeared total, ${newlyArchivedOnly.length} genuinely new (${alreadyArchivedSet.size} already archived, skipped)`); idsToCheck = newlyArchivedOnly; } - classificationBreakdown = await runBUDriftChecker(idsToCheck, apiKey, clientId, skipTls); + classificationBreakdown = await runBUDriftChecker(idsToCheck, apiKey, clientId, skipTls, previousBuMap); } catch (err) { console.error('[Ivanti Findings] BU drift checker failed (non-fatal):', err.message); } diff --git a/backend/scripts/check-host-fields.js b/backend/scripts/check-host-fields.js new file mode 100644 index 0000000..aee2eaf --- /dev/null +++ b/backend/scripts/check-host-fields.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +// Temporary diagnostic script — fetch a specific finding and dump host fields +require('dotenv').config(); +const { ivantiPost } = require('../helpers/ivantiApi'); + +const apiKey = process.env.IVANTI_API_KEY; +const clientId = process.env.IVANTI_CLIENT_ID || '1550'; +const skipTls = process.env.IVANTI_SKIP_TLS === 'true'; + +const findingId = process.argv[2] || '2814870699'; + +const urlPath = `/client/${encodeURIComponent(clientId)}/hostFinding/search`; +const body = { + filters: [ + { field: 'id', exclusive: false, operator: 'EXACT', orWithPrevious: false, implicitFilters: [], value: findingId, caseSensitive: false } + ], + projection: 'internal', + sort: [{ field: 'severity', direction: 'ASC' }], + page: 0, + size: 1 +}; + +ivantiPost(urlPath, body, apiKey, skipTls).then(r => { + const data = JSON.parse(r.body); + const finding = (data._embedded && data._embedded.hostFindings || [])[0]; + if (!finding) { console.log('Finding not found'); process.exit(0); } + + console.log('=== host object ==='); + console.log(JSON.stringify(finding.host, null, 2)); + console.log(''); + console.log('=== hostAdditionalDetails ==='); + console.log(JSON.stringify(finding.hostAdditionalDetails, null, 2)); + process.exit(0); +}).catch(e => { console.error(e.message); process.exit(1); }); diff --git a/docs/architecture/ad-saml-integration.md b/docs/architecture/ad-saml-integration.md new file mode 100644 index 0000000..b2a0cf2 --- /dev/null +++ b/docs/architecture/ad-saml-integration.md @@ -0,0 +1,354 @@ +# AD/SAML Integration Architecture + +## Overview + +This document describes the architecture for integrating Active Directory (AD) authentication via SAML 2.0 into the STEAM Security Dashboard. The integration adds Single Sign-On (SSO) as the primary authentication method while retaining local password login as a break-glass fallback for administrators. AD group memberships drive automatic permission assignment and BU team scoping through a configurable mapping layer. + +--- + +## Authentication Model + +The dashboard supports two authentication paths simultaneously: + +| Path | Users | Mechanism | Session | +|---|---|---|---| +| Local | Break-glass admins, service accounts | Username + bcrypt password | Cookie-based, PostgreSQL sessions table | +| SAML SSO | All AD users | SP-initiated SAML 2.0 via AD FS | Same cookie-based session (identical to local) | + +Both paths produce the same session artifact — an httpOnly cookie containing a `session_id` that maps to a row in the `sessions` table. Downstream middleware (`requireAuth`, `requireGroup`) is unaware of how the session was created. + +--- + +## SAML 2.0 Authentication Flow + +### SP-Initiated Login (Success Path) + +```mermaid +sequenceDiagram + participant B as Browser + participant SP as Dashboard (SP) + participant IdP as AD FS (IdP) + participant DB as PostgreSQL + + B->>SP: GET /api/auth/saml/login + SP->>SP: Generate AuthnRequest XML + SP->>B: HTTP 302 Redirect to IdP SSO URL (with AuthnRequest) + B->>IdP: Follow redirect (user sees AD FS login page) + IdP->>IdP: Authenticate user against AD + IdP->>IdP: Build assertion (NameID, email, groups) + IdP->>IdP: Sign assertion with IdP private key + IdP->>B: HTTP 200 with auto-submit form (POST to SP callback) + B->>SP: POST /api/auth/saml/callback (SAMLResponse in body) + SP->>SP: Base64-decode SAMLResponse + SP->>SP: Validate XML signature against IdP certificate + SP->>SP: Check NotBefore/NotOnOrAfter (120s clock skew tolerance) + SP->>SP: Extract NameID, email, displayName, group claims + SP->>DB: Look up user by external_id (NameID) + alt New user (no matching external_id) + SP->>DB: INSERT into users (JIT provisioning) + SP->>DB: INSERT audit_log (saml_user_provisioned) + else Existing user + SP->>DB: UPDATE user group, teams, email + SP->>DB: INSERT audit_log (saml_user_updated) if changed + end + SP->>DB: INSERT into sessions (session_id, user_id, expires_at) + SP->>DB: INSERT audit_log (saml_login) + SP->>B: Set-Cookie: session_id=xxx; HttpOnly; SameSite=Lax + SP->>B: HTTP 302 Redirect to /?saml_success=true + B->>SP: GET /api/auth/me (with cookie) + SP->>B: 200 { user: { id, username, group, teams, authSource } } +``` + +### Assertion Rejection Path + +```mermaid +sequenceDiagram + participant B as Browser + participant SP as Dashboard (SP) + participant IdP as AD FS (IdP) + + B->>SP: GET /api/auth/saml/login + SP->>B: HTTP 302 Redirect to IdP + B->>IdP: Authenticate + IdP->>B: POST assertion to SP callback + B->>SP: POST /api/auth/saml/callback + SP->>SP: Validate assertion + alt Invalid signature + SP->>SP: Log audit (saml_auth_failed, reason: invalid_signature) + SP->>B: Redirect /?saml_error=Invalid+assertion+signature + else Expired assertion + SP->>SP: Log audit (saml_auth_failed, reason: assertion_expired) + SP->>B: Redirect /?saml_error=Assertion+expired + else Account disabled + SP->>SP: Log audit (saml_auth_failed, reason: account_disabled) + SP->>B: Redirect /?saml_error=Account+is+disabled + end +``` + +--- + +## Component Architecture + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ Express Backend (port 3001) │ +│ │ +│ ┌────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ +│ │ routes/saml.js │ │ routes/auth.js │ │ middleware/auth.js│ │ +│ │ │ │ │ │ │ │ +│ │ GET /status │ │ POST /login │ │ requireAuth() │ │ +│ │ GET /login │ │ POST /logout │ │ requireGroup() │ │ +│ │ POST /callback │ │ GET /me │ │ │ │ +│ │ GET /metadata │ │ POST /change-pw │ │ (unchanged — │ │ +│ └───────┬────────┘ └────────┬────────┘ │ reads session │ │ +│ │ │ │ cookie only) │ │ +│ │ │ └──────────────────┘ │ +│ ▼ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ helpers/samlProvisioning.js │ │ +│ │ │ │ +│ │ resolveGroup(adGroups, config) → dashboardGroup │ │ +│ │ resolveTeams(adGroups, config) → "STEAM,..." │ │ +│ │ deriveUsername(nameId) → username │ │ +│ │ provisionOrUpdateUser(assertion, config, ip) │ │ +│ └───────────────────────┬───────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ helpers/samlConfig.js │ │ +│ │ │ │ +│ │ loadGroupMappingConfig() → validated config obj │ │ +│ │ (reads config/adGroupMapping.json or env var) │ │ +│ └───────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +### Key Design Decisions + +1. **Session reuse**: SAML login creates the exact same session format as local login. No changes to `requireAuth()` middleware. +2. **Feature flag isolation**: When `SAML_ENABLED=false`, SAML routes return 404 and no SAML library is loaded. Zero runtime cost when disabled. +3. **Config-driven mapping**: AD group names are externalized in `config/adGroupMapping.json`. Changing the mapping requires only a file edit and backend restart — no code changes. +4. **JIT provisioning**: Users are created on first login, updated on each subsequent login. AD is the source of truth for SSO users. +5. **Separation of concerns**: The provisioning logic (`samlProvisioning.js`) is a pure module with no HTTP dependencies — fully unit-testable without a web server. + +--- + +## AD Group-to-Permission Mapping + +### Configuration Structure + +```json +{ + "groups": { + "": "", + "CVE-Dashboard-Admins": "Admin", + "CVE-Dashboard-Users": "Standard_User", + "CVE-Dashboard-Leadership": "Leadership", + "CVE-Dashboard-ReadOnly": "Read_Only" + }, + "teams": { + "": "", + "NTS-AEO-STEAM": "STEAM", + "NTS-AEO-ACCESS-ENG": "ACCESS-ENG", + "NTS-AEO-ACCESS-OPS": "ACCESS-OPS", + "NTS-AEO-INTELDEV": "INTELDEV" + }, + "groupPriority": ["Admin", "Standard_User", "Leadership", "Read_Only"], + "defaultGroup": "Read_Only", + "attributes": { + "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "displayName": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", + "groups": "http://schemas.xmlsoap.org/claims/Group" + } +} +``` + +### Privilege Hierarchy + +When a user belongs to multiple AD groups that map to different dashboard groups, the highest-privilege group wins: + +``` +Admin > Standard_User > Leadership > Read_Only +``` + +### Multi-Team Assignment + +When a user belongs to multiple AD groups that map to BU teams, all matching teams are assigned: + +``` +AD Groups: ["NTS-AEO-STEAM", "NTS-AEO-ACCESS-ENG", "CVE-Dashboard-Users"] +→ user_group: "Standard_User" +→ bu_teams: "ACCESS-ENG,STEAM" (sorted alphabetically, deduplicated) +``` + +### Placeholder Group Names + +The AD group names in the `groups` and `teams` sections (e.g., "CVE-Dashboard-Admins") are placeholders. When real group CNs are obtained from the AD administrators, update only this configuration file. No code changes required. + +--- + +## Database Schema Changes + +### Migration: `add_saml_auth_columns.js` + +| Column | Type | Nullable | Default | Purpose | +|---|---|---|---|---| +| `auth_source` | VARCHAR(10) | NOT NULL | `'local'` | Discriminates local vs SSO users | +| `external_id` | VARCHAR(256) | NULL | NULL | SAML NameID for IdP correlation | + +Additional changes: +- `password_hash` becomes nullable (SAML users have no local password) +- Partial unique index on `external_id WHERE external_id IS NOT NULL` + +### Impact on Existing Data + +- All existing users receive `auth_source = 'local'` and `external_id = NULL` +- No existing functionality is affected +- Migration is idempotent (safe to re-run) + +--- + +## Environment Variables + +| Variable | Required | Description | Example | +|---|---|---|---| +| `SAML_ENABLED` | Always | Master feature flag for SAML authentication | `false` | +| `SAML_IDP_METADATA_URL` | When SAML_ENABLED=true | AD FS federation metadata endpoint | `https://adfs.corp.local/FederationMetadata/2007-06/FederationMetadata.xml` | +| `SAML_SP_ENTITY_ID` | When SAML_ENABLED=true | Unique identifier for this Service Provider | `http://71.85.90.6:3001` | +| `SAML_SP_CALLBACK_URL` | When SAML_ENABLED=true | Assertion consumer service URL | `http://71.85.90.6:3001/api/auth/saml/callback` | +| `SAML_IDP_CERT_PATH` | When SAML_ENABLED=true | File path to IdP signing certificate (PEM format) | `/etc/cve-dashboard/idp-cert.pem` | +| `SESSION_LIFETIME_HOURS` | Optional | Session duration (1-720 hours, default: 24) | `8` | +| `AD_GROUP_MAPPING_JSON` | Optional | JSON string override for adGroupMapping.json | `{"groups":{...},"teams":{...}}` | + +### Startup Validation + +When `SAML_ENABLED=true`, the server validates at startup: +1. All required SAML env vars are set (fails with descriptive error if missing) +2. Certificate file exists and is readable (fails if not) +3. Group mapping config parses as valid JSON (fails if not) +4. All mapped team names exist in KNOWN_TEAMS (fails if not) +5. All mapped dashboard groups are valid (fails if not) + +This fail-fast approach prevents silent misconfiguration in production. + +--- + +## Build vs Wait: Phase Breakdown + +### Phase 1 — Build Now (No AD Access Required) + +| Component | File | Test Strategy | +|---|---|---| +| Database migration | `backend/migrations/add_saml_auth_columns.js` | Run against test DB, verify columns | +| Group mapping config | `backend/config/adGroupMapping.json` | Startup validation tests | +| Config loader | `backend/helpers/samlConfig.js` | Unit test with mock JSON files | +| JIT provisioner | `backend/helpers/samlProvisioning.js` | Unit test all paths with mock pool | +| SAML routes (skeleton) | `backend/routes/saml.js` | Integration test: feature flag, status, metadata | +| Session lifetime | `server.js` (startup block) | Unit test env var parsing | +| Auth route changes | `backend/routes/auth.js` | Integration test: SAML user login rejection | +| User route changes | `backend/routes/users.js` | Integration test: auth_source in responses, password block | +| Frontend SSO button | `frontend/src/components/LoginForm.js` | Render test: button hidden when flag=false | +| Admin auth_source badges | `frontend/src/components/UserManagement.js` | Render test: badge displays | +| Architecture doc | `docs/architecture/ad-saml-integration.md` | N/A (documentation) | + +### Phase 2 — Requires Live AD FS Connection + +| Component | Dependency | Who Provides It | +|---|---|---| +| SAML library installation | Package selection (`@node-saml/passport-saml` or `saml2-js`) | Development team | +| IdP metadata URL | AD FS federation metadata endpoint | AD administrators | +| IdP signing certificate | Token-signing cert exported from AD FS | AD administrators | +| SP registration | Relying party trust created in AD FS console | AD administrators | +| Real AD group names | Actual CNs of permission/team groups | AD administrators | +| Assertion parsing implementation | Fill in `routes/saml.js` callback | Development team | +| End-to-end flow testing | Working AD user accounts | AD administrators | +| Session lifetime tuning | AD FS token lifetime policy value | AD administrators | + +--- + +## Security Considerations + +### Certificate Management + +- The IdP signing certificate is stored on disk at `SAML_IDP_CERT_PATH` +- When the IdP rotates its certificate, replace the file and restart the backend +- No database migration required for certificate rotation +- Consider monitoring certificate expiry dates (AD FS certs typically rotate annually) + +### Assertion Replay Prevention + +- Each SAML assertion is consumed exactly once by the callback handler +- The JIT provisioner's idempotent update pattern means replayed assertions would simply re-update the same user record (no escalation possible) +- For additional protection in Phase 2, implement InResponseTo validation and a short-lived assertion ID cache + +### Trust Boundary + +``` +┌─────────────────────────────────┐ ┌─────────────────────────────────┐ +│ Trust Zone A │ │ Trust Zone B │ +│ │ │ │ +│ Dashboard (SP) │ │ AD FS (IdP) │ +│ - Validates assertions │ │ - Authenticates users │ +│ - Trusts ONLY signed assertions │ │ - Signs assertions with │ +│ - Creates local sessions │ │ private key │ +│ - Enforces local authorization │ │ - Asserts group memberships │ +│ │ │ │ +└────────────────┬─────────────────┘ └────────────────┬────────────────┘ + │ │ + └─── SAML 2.0 over HTTPS (HTTP-POST) ────┘ +``` + +- The SP trusts assertions only when cryptographically signed by the IdP +- Group memberships in the assertion drive permission assignment — the SP does not query AD directly +- If the IdP is compromised, an attacker could forge assertions. Mitigate with certificate pinning and monitoring assertion patterns in audit logs. +- The SP never sends credentials to the IdP — authentication happens entirely on the IdP side + +### Break-Glass Protection + +- The last local Admin account cannot be deleted or deactivated +- If the IdP is unavailable, local Admin users can still log in with username/password +- SAML users cannot authenticate via password (and vice versa) — the two paths are isolated per user record + +### Transport Security + +- Production deployments should serve the SP callback over HTTPS +- The SAMLResponse is transmitted via HTTP-POST binding (browser-mediated, not direct server-to-server) +- The assertion is signed — even if transmitted over HTTP, it cannot be tampered with without detection +- For defense in depth, HTTPS prevents assertion interception by network observers + +--- + +## Hybrid Login User Experience + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Login Page │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ Sign in with SSO │ │ │ +│ │ │ (redirects to AD FS) │ │ │ +│ │ └─────────────────────────────┘ │ │ +│ │ │ │ +│ │ ─── or sign in with local account ─── │ │ +│ │ │ │ +│ │ Username: [________________] │ │ +│ │ Password: [________________] │ │ +│ │ │ │ +│ │ [Sign In] │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ Note: SSO button only visible when SAML_ENABLED=true │ +│ Local login always available (break-glass for admins) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Related Specifications + +- `.kiro/specs/ad-saml-integration/requirements.md` — detailed acceptance criteria +- `.kiro/specs/ad-saml-integration/design.md` — implementation design with code examples +- `.kiro/specs/group-based-access-control/requirements.md` — existing RBAC system +- `.kiro/specs/multi-bu-tenancy/design.md` — BU team scoping (leveraged by AD integration) diff --git a/docs/architecture/split-architecture-proposal.md b/docs/architecture/split-architecture-proposal.md new file mode 100644 index 0000000..c50ff67 --- /dev/null +++ b/docs/architecture/split-architecture-proposal.md @@ -0,0 +1,547 @@ +# Split Architecture Proposal: Collector + Indexer + +**Author:** Infrastructure Team +**Date:** 2026-06-08 +**Status:** Draft — Pending Review +**Scope:** Scale CVE Dashboard from 2 teams / ~15 users to company-wide deployment (100+ users, 15+ teams) + +--- + +## Executive Summary + +The STEAM Security Dashboard currently runs as a monolithic single-process Express application on CT107 (dashboard-dev, 71.85.90.9). This single process simultaneously serves the frontend, handles all API requests, and performs background data collection from Ivanti, Jira, CARD, Atlas, and NVD APIs. + +At current scale (2 teams, <15 users, daily sync), this architecture works. At company-wide scale (15+ teams, hundreds of users, sub-hourly sync), it will not. This document proposes a phased transition to a **Collector + API Server** architecture that separates data ingestion from request serving. + +**Critical constraint:** CT107 (71.85.90.9) has the firewall rules granting access to the production Ivanti, Jira, and CARD APIs. The collector component must remain on this machine or firewall rules must be extended. + +--- + +## Table of Contents + +- [Current Architecture](#current-architecture) +- [Problem Statement](#problem-statement) +- [Proposed Architecture](#proposed-architecture) +- [Phase Plan](#phase-plan) +- [Infrastructure Requirements](#infrastructure-requirements) +- [Risk Assessment](#risk-assessment) +- [Decision Points](#decision-points) +- [Appendix: Current Data Flow Analysis](#appendix-current-data-flow-analysis) + +--- + +## Current Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CT107 (dashboard-dev) │ +│ 71.85.90.9 — 48 GB RAM, 250 GB Disk │ +│ │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ Express Process (port 3001/3100) │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │ │ +│ │ │ React SPA │ │ API Routes │ │ Sync Workers │ │ │ +│ │ │ (static) │ │ (50+ endpts)│ │ (setInterval) │ │ │ +│ │ └─────────────┘ └──────────────┘ └────────────────┘ │ │ +│ │ │ │ │ │ +│ │ │ Shared PG Pool (10 conn) │ │ +│ │ │ │ │ │ +│ └──────────────────────────┼──────────┼─────────────────────┘ │ +│ │ │ │ +│ ┌──────────────────────────▼──────────▼─────────────────────┐ │ +│ │ PostgreSQL 16 (Docker, port 5433) │ │ +│ └───────────────────────────────────────────────────────────┘ │ +│ │ +│ Firewall Access: Ivanti API, Jira DC, CARD API, Atlas API │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key Metrics (Current) + +| Metric | Current Value | Company-Wide Projection | +|--------|--------------|------------------------| +| Concurrent users | 5–15 | 100–300 | +| Teams tracked | 2 | 15+ | +| Ivanti findings (open) | ~200–500 | 2,000–10,000+ | +| Ivanti sync frequency | 24h | 1–4h desired | +| PG connection pool | 10 | Insufficient | +| Jira API rate limit | 1,440/day | Shared across all users | +| Data sources | 5 (Ivanti, NVD, Jira, Atlas, CARD) | 8+ (add CrowdStrike, Qualys, Tanium) | + +--- + +## Problem Statement + +### 1. Sync Blocks the API Server + +`syncFindings()` runs sequentially through: +1. Fetch all open findings pages (100/page) +2. Upsert findings batch into PostgreSQL +3. Detect archive changes (compare all previous vs current) +4. Fetch all closed findings pages +5. Upsert closed findings +6. Run BU drift checker (makes additional API calls per disappeared finding) +7. Sync FP workflow counts (sweeps all closed pages again) +8. Compute and store anomaly summary +9. Record counts history + +At 500 findings, this takes 2–5 minutes. At 10,000 findings across 15 teams, this could take 15–30 minutes. During sync, the Express process is saturated — API responses slow, the connection pool contends. + +### 2. Single Point of Failure + +One process handles everything. A memory leak during sync, an unhandled promise rejection in the BU drift checker, or a runaway loop in archive detection crashes the entire dashboard for all users. + +### 3. Connection Pool Exhaustion + +10 connections shared between: +- User-facing read queries (findings list, compliance items, charts) +- Sync bulk upserts (batches of 100 rows × 18 columns) +- User writes (notes, overrides, queue operations) + +The pool already logs warnings at 8/10 active. At 100+ concurrent users issuing reads while a sync writes thousands of rows, this will deadlock or time out. + +### 4. Rate Limits Shared Across Functions + +Jira's 1,440/day limit is consumed by both background sync and user-initiated operations (lookups, ticket creation). A bulk sync could exhaust the daily budget, blocking users from creating tickets the rest of the day. + +### 5. No Horizontal Scaling Path + +Cannot add a second API server without also duplicating the sync scheduler, which would cause duplicate syncs, double-writes, and race conditions. + +### 6. Firewall Constraint + +CT107 has the only firewall access to production Ivanti, Jira, and CARD APIs. The collector (data fetcher) must run on this machine. The API server could potentially move elsewhere, but the collector cannot without firewall changes. + +--- + +## Proposed Architecture + +### Target State + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CT107 (dashboard-dev) │ +│ 71.85.90.9 — 48 GB RAM, 250 GB Disk │ +│ ★ Firewall access to prod APIs ★ │ +│ │ +│ ┌───────────────────────────────────┐ ┌─────────────────────┐│ +│ │ API Server (Express, port 3001) │ │ Collector Service ││ +│ │ │ │ (Node.js worker) ││ +│ │ • React SPA serving │ │ ││ +│ │ • All /api/* read endpoints │ │ • Ivanti sync ││ +│ │ • User writes (notes, queue) │ │ • Jira bulk sync ││ +│ │ • On-demand lookups (proxied) │ │ • CARD cache sync ││ +│ │ • Triggers collector via │ │ • Atlas cache sync ││ +│ │ pg NOTIFY │ │ • NVD bulk sync ││ +│ │ │ │ • Archive detect ││ +│ │ Pool: 15 conn (reads + writes) │ │ • BU drift checker ││ +│ │ │ │ • Anomaly compute ││ +│ └───────────────┬───────────────────┘ │ • Compliance parse ││ +│ │ │ ││ +│ │ │ Pool: 10 conn ││ +│ │ │ (bulk upserts) ││ +│ │ │ ││ +│ │ │ Listens: ││ +│ │ │ pg LISTEN ││ +│ │ │ 'sync_trigger' ││ +│ │ └──────────┬──────────┘│ +│ │ │ │ +│ ┌───────────────▼──────────────────────────────────▼─────────┐│ +│ │ PostgreSQL 16 (Docker, port 5433) ││ +│ │ Pool: 25 total connections allocated ││ +│ └────────────────────────────────────────────────────────────┘│ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Component Responsibilities + +#### API Server (`cve-api.service`) + +| Responsibility | Details | +|---|---| +| Frontend serving | Static React build via `express.static` | +| Read endpoints | All GET routes — findings, compliance, charts, exports | +| User writes | Notes, overrides, queue items, ticket CRUD, KB uploads | +| On-demand lookups | Single NVD lookup, single Jira issue lookup, CARD real-time queries | +| Sync trigger | `SELECT pg_notify('sync_trigger', '{"type":"findings","user":"admin"}')` | +| Health/status | Expose collector status via sync_state table reads | + +#### Collector (`cve-collector.service`) + +| Responsibility | Details | +|---|---| +| Scheduled syncs | Ivanti findings (configurable interval), workflows (24h) | +| Bulk API operations | Jira JQL sync-all, Atlas cache refresh, NVD bulk sync | +| Post-sync processing | Archive detection, BU drift classification, closed-gone detection | +| Anomaly computation | Open/closed deltas, classification breakdown, significance flagging | +| Compliance parsing | Spawns Python subprocess for xlsx parsing on upload commit | +| Event-driven triggers | Listens on `pg LISTEN sync_trigger` for on-demand requests | +| Rate budget management | Owns the Jira daily/burst counters; API server gets a reserved allocation | + +### Communication Pattern + +``` +User clicks "Sync" in UI + │ + ▼ +API Server receives POST /api/ivanti/findings/sync + │ + ▼ +API Server: SELECT pg_notify('sync_trigger', '{"type":"findings"}') + │ + ▼ +API Server responds: { status: 'sync_started', message: 'Check /sync-status' } + │ + ▼ +Collector receives NOTIFY, starts syncFindings() + │ + ▼ +Collector updates ivanti_sync_state (status='syncing') + │ + ▼ +Collector completes, updates ivanti_sync_state (status='success') + │ + ▼ +Frontend polls GET /api/ivanti/findings/sync-status → sees 'success' → refreshes +``` + +No Redis. No message broker. Just PostgreSQL LISTEN/NOTIFY — zero new infrastructure. + +--- + +## Phase Plan + +### Phase 0: Immediate Improvements (Week 1–2) +**Goal:** Reduce risk within the current monolith. No architectural changes. + +| Task | Effort | Impact | +|------|--------|--------| +| Make `POST /sync` non-blocking — return immediately, let sync run in background | 2h | Unblocks users during sync | +| Add `GET /api/ivanti/findings/sync-status` endpoint | 1h | Frontend can poll for completion | +| Increase PG pool from 10 → 20 connections | 10min | Headroom for concurrent operations | +| Add `pg_stat_activity` monitoring query to health endpoint | 30min | Visibility into pool pressure | +| Update frontend to poll sync-status instead of waiting | 2h | UX improvement | + +**Deliverables:** +- Updated `ivantiFindings.js` with async sync dispatch +- New sync-status polling endpoint +- Frontend ReportingPage sync UX updated +- Pool configuration change in `db.js` + +--- + +### Phase 1: Extract Collector (Weeks 3–4) +**Goal:** Separate data collection into its own process on CT107. + +| Task | Effort | Impact | +|------|--------|--------| +| Create `backend/collector.js` — standalone Node process | 4h | Fault isolation | +| Move sync functions from route files into shared `lib/sync/` modules | 3h | Code reuse between collector and API | +| Implement pg LISTEN/NOTIFY trigger mechanism | 2h | API → Collector communication | +| Create `cve-collector.service` systemd unit | 30min | Process management | +| Add collector health check and status reporting | 1h | Observability | +| Update `POST /sync` routes to use pg_notify instead of inline sync | 1h | Complete decoupling | +| Add `sync_jobs` table for job tracking (queued, running, complete, failed) | 1h | Multi-user sync coordination | +| Update CI/CD pipeline to deploy collector service | 2h | Automated deployment | + +**Deliverables:** +- `backend/collector.js` — entry point for collector process +- `backend/lib/sync/` — shared sync logic (extracted from routes) +- `systemd/cve-collector.service` — systemd unit +- Updated `.gitlab-ci.yml` with collector deploy stage +- `sync_jobs` table for job state tracking + +**File structure after Phase 1:** + +``` +backend/ +├── server.js # API server (unchanged entry point) +├── collector.js # NEW — collector entry point +├── db.js # Shared pool config +├── lib/ +│ └── sync/ +│ ├── ivantiFindings.js # Extracted from routes/ivantiFindings.js +│ ├── ivantiWorkflows.js # Extracted from routes/ivantiWorkflows.js +│ ├── jiraBulkSync.js # Extracted from routes/jiraTickets.js +│ ├── atlasCache.js # Extracted from routes/atlas.js +│ ├── nvdBulkSync.js # New — bulk NVD operations +│ ├── archiveDetection.js # Extracted from routes/ivantiFindings.js +│ └── anomalyCompute.js # Extracted from routes/ivantiFindings.js +├── routes/ # API routes — now thin, read-heavy +└── helpers/ # Shared API client helpers (unchanged) +``` + +--- + +### Phase 2: Multi-Tenancy & Scale Hardening (Weeks 5–8) +**Goal:** Prepare for 15 teams and hundreds of users. + +| Task | Effort | Impact | +|------|--------|--------| +| Per-team sync scheduling — stagger syncs to avoid API burst | 3h | Spreads load | +| Jira rate budget partitioning (collector gets 80%, API gets 20%) | 2h | Prevents sync from starving users | +| Per-BU finding isolation — team users only see their findings | 4h | Data scoping | +| Add connection pooling metrics endpoint (`/api/admin/pool-stats`) | 1h | Operational visibility | +| Implement sync queue with priority (user-triggered > scheduled) | 3h | Better UX | +| Add retry logic with exponential backoff to collector | 2h | Resilience | +| Partial-progress persistence — don't lose work on mid-sync failure | 4h | Data integrity | +| PG connection pool separation — API pool (15) + Collector pool (10) | 1h | Isolation | +| Add `pg_bouncer` or similar for connection multiplexing (optional) | 4h | Scale past 50 concurrent | + +**Deliverables:** +- Team-scoped sync scheduler in collector +- Rate budget allocation system +- Retry/backoff logic +- Partial progress tracking +- Pool separation + +--- + +### Phase 3: Additional Data Sources (Weeks 9–12) +**Goal:** Integrate CrowdStrike, Qualys, and Tanium feeds. + +| Task | Effort | Impact | +|------|--------|--------| +| CrowdStrike Falcon API integration in collector | 8h | New vulnerability source | +| Qualys VMDR API integration in collector | 8h | New vulnerability source | +| Tanium asset inventory sync | 6h | Asset correlation | +| Cross-source finding deduplication logic | 6h | Data quality | +| Unified findings view (merged from all sources) | 4h | Single pane of glass | +| Source-specific sync schedules (configurable per source) | 2h | Flexibility | + +**Note:** All new API integrations go into the collector. The API server never makes outbound calls to external vulnerability platforms except for single-item on-demand lookups. + +**Firewall implications:** CrowdStrike, Qualys, and Tanium API access will need firewall rules added to CT107 (71.85.90.9). Submit firewall requests in advance. + +--- + +### Phase 4: Horizontal Scaling (Weeks 13+) +**Goal:** Support 300+ concurrent users if company-wide adoption materializes. + +| Task | Effort | Impact | +|------|--------|--------| +| Move API server to a separate LXC container (with more resources) | 4h | Dedicated API resources | +| Run multiple API server instances behind a load balancer | 8h | Horizontal scale | +| Keep collector on CT107 (firewall access) | 0h | No change needed | +| Add Redis for session store (replace PG sessions) | 4h | Multi-instance sessions | +| Add read replicas if PG becomes the bottleneck | 8h | Read scale | +| Evaluate moving PG to CT109 (zbl-indexer, 32GB/500GB) | 2h | Larger DB host | + +**Architecture at Phase 4:** + +``` + ┌─────────────────┐ + │ Load Balancer │ + │ (nginx/HAProxy)│ + └────┬───────┬────┘ + │ │ + ┌─────────────▼─┐ ┌─▼─────────────┐ + │ API Server 1 │ │ API Server 2 │ (New LXC or CT103) + │ (Express) │ │ (Express) │ + └───────┬───────┘ └───────┬───────┘ + │ │ + └─────────┬─────────┘ + │ +┌────────────────────────────▼──────────────────────────────────────┐ +│ CT107 (71.85.90.9) │ +│ │ +│ ┌─────────────────────────┐ ┌──────────────────────────────┐ │ +│ │ Collector Service │ │ PostgreSQL 16 │ │ +│ │ (sole process with │ │ (or moved to CT109) │ │ +│ │ firewall API access) │ │ │ │ +│ └─────────────────────────┘ └──────────────────────────────┘ │ +│ │ +│ ★ Firewall: Ivanti, Jira, CARD, Atlas, CrowdStrike, Qualys ★ │ +└───────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Infrastructure Requirements + +### CT107 Resource Allocation (Current → Phase 2) + +| Resource | Current | Phase 2 Target | Notes | +|----------|---------|---------------|-------| +| RAM | 48 GB | 48 GB (sufficient) | Node processes use <2GB each | +| CPU | Shared | May need 4+ dedicated cores | Sync is CPU-intensive during transform | +| Disk | 250 GB | 250 GB (sufficient) | PG data + uploads + logs | +| PG connections | 10 | 25 (15 API + 10 collector) | Configure in `postgresql.conf` | +| Systemd services | 2 (backend + frontend) | 3 (api + collector + postgres) | Frontend served by API | + +### PostgreSQL Tuning (for 15 teams / hundreds of users) + +``` +# postgresql.conf changes +max_connections = 50 # Up from default 100 is fine, need headroom +shared_buffers = 4GB # 25% of available RAM for PG +effective_cache_size = 12GB # 75% of RAM PG can expect from OS +work_mem = 64MB # Per-sort/hash operation +maintenance_work_mem = 512MB # For VACUUM, CREATE INDEX +wal_level = replica # If read replicas needed later +``` + +### Firewall Dependencies + +| Service | Endpoint | Required By | Current Access | +|---------|----------|-------------|----------------| +| Ivanti/RiskSense | platform4.risksense.com:443 | Collector | ✅ CT107 only | +| Jira Data Center | jira.charter.com:443 | Collector + API (lookups) | ✅ CT107 only | +| CARD API | card.charter.com:443 | API (real-time) | ✅ CT107 only | +| Atlas InfoSec | (internal) | Collector | ✅ CT107 only | +| NVD API | services.nvd.nist.gov:443 | Collector + API | ✅ Public | +| CrowdStrike | api.crowdstrike.com:443 | Collector | ❌ Firewall request needed | +| Qualys | qualysapi.qualys.com:443 | Collector | ❌ Firewall request needed | +| Tanium | (internal) | Collector | ❌ Firewall request needed | + +**Key constraint:** If the API server moves off CT107 in Phase 4, you'll need firewall rules for the new host to reach Jira (for user lookups) and CARD (for real-time queries). Alternatively, the collector could proxy those on-demand requests — adds latency but avoids firewall changes. + +--- + +## Risk Assessment + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|------------| +| Collector crash doesn't affect API users | — | — | This is the primary benefit of splitting | +| Collector and API race on DB writes | Medium | Low | Collector does bulk upserts; API does single-row writes. Different tables mostly. Use advisory locks for sync_state. | +| Sync trigger lost (pg NOTIFY missed) | Low | Medium | Collector also runs on a schedule. Missed trigger just delays to next interval. | +| Phase 1 introduces bugs in extraction | Medium | Medium | Comprehensive test suite exists. Run parallel (old monolith + new split) in staging for 1 week. | +| Firewall change delays block Phase 4 | High | Medium | Start firewall requests early. Phase 4 is optional — single-machine split (Phases 1–3) works fine at 15 teams. | +| PG becomes bottleneck at 300+ users | Low | High | Phase 4 addresses with read replicas. CT109 (500GB, 32GB) available as larger DB host. | + +--- + +## Decision Points + +These require team/leadership input before proceeding: + +1. **Sync frequency target:** Is 1-hour sync acceptable, or do teams need near-real-time (15 min)? This affects collector design complexity and API rate budget math. + +2. **API server location:** Keep everything on CT107, or move the API server to a separate container? Keeping it on CT107 is simpler (no firewall changes for CARD/Jira lookups) but limits scaling options. + +3. **Database location:** Keep PG on CT107, or move to CT109 (zbl-indexer, 500GB disk, 32GB RAM)? Moving adds network latency but gives more room for growth. + +4. **CrowdStrike/Qualys/Tanium priority:** Which new data sources are most urgent? This affects Phase 3 ordering and firewall request timing. + +5. **Session management:** At 300+ users, PG-backed sessions will be high-churn. Acceptable, or invest in Redis? Redis adds infrastructure but is the industry standard for session stores at scale. + +6. **Multi-instance API:** Is the goal to survive a single API server restart without downtime? If yes, Phase 4 (load balancer + multiple instances) is needed. If brief restarts during deploys are acceptable, single-instance on CT107 works through Phase 3. + +--- + +## Appendix: Current Data Flow Analysis + +### Data Collection Patterns + +| Source | Trigger | Frequency | Data Volume | Processing | +|--------|---------|-----------|-------------|------------| +| Ivanti Findings | Schedule + manual | 24h | 100–500 findings (all pages) | Extract, upsert, archive detect, BU drift, anomaly | +| Ivanti Workflows | Schedule + manual | 24h | 50 workflow batches | Store as JSON blob | +| Ivanti Closed Findings | During findings sync | 24h | All closed pages | Upsert + closed archive detection | +| Jira Bulk Sync | Manual (admin) | On-demand | All tracked tickets via JQL | Status/summary update per ticket | +| Jira Single Lookup | User action | Real-time | 1 issue | Proxy + display | +| NVD Lookup | User action | Real-time | 1 CVE | Proxy + optional save | +| NVD Bulk Sync | Manual | On-demand | All CVEs in DB | Batch update metadata | +| Atlas Action Plans | Cache refresh | Background | Per-host plan data | Cache in `atlas_action_plans_cache` | +| CARD Operations | User action | Real-time | 1 asset at a time | Proxy (confirm/decline/redirect) | +| Compliance xlsx | Manual upload | Weekly | 1 file → hundreds of rows | Python parse → PG upsert (transactional) | + +### What Moves to Collector vs Stays in API + +| Operation | Collector | API Server | Rationale | +|-----------|-----------|------------|-----------| +| Ivanti findings sync (all pages) | ✅ | | Heavy, multi-page, post-processing | +| Ivanti workflows sync | ✅ | | Scheduled background task | +| Ivanti closed sweep | ✅ | | Part of findings sync pipeline | +| Archive detection | ✅ | | CPU-intensive comparison | +| BU drift checker | ✅ | | Makes additional API calls | +| Anomaly computation | ✅ | | Depends on sync completion | +| Jira bulk sync-all | ✅ | | Consumes rate budget, multi-issue | +| NVD bulk sync | ✅ | | Multi-CVE, rate-limited | +| Atlas cache refresh | ✅ | | Background, per-host API calls | +| Compliance xlsx parse | ✅ | | Spawns Python, heavy DB writes | +| Single Jira lookup | | ✅ | User-initiated, real-time, 1 call | +| Single NVD lookup | | ✅ | User-initiated, real-time, 1 call | +| CARD operations | | ✅ | User-initiated, real-time | +| All GET /api/* reads | | ✅ | Pure DB queries, user-facing | +| Notes/overrides/queue | | ✅ | Small writes, user-facing | +| File uploads | | ✅ | User-initiated, disk I/O | + +### Sync Pipeline Detail (becomes collector's core loop) + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Collector Sync Pipeline │ +│ │ +│ ┌────────────────┐ │ +│ │ 1. Fetch Open │ ← Ivanti API (paginated, 100/page) │ +│ │ Findings │ │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 2. Extract & │ ← Transform raw API → normalized rows │ +│ │ Transform │ │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 3. Upsert to │ ← Batch INSERT ON CONFLICT (100/batch) │ +│ │ PG │ Preserves notes + overrides │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 4. Archive │ ← Compare previous IDs vs current IDs │ +│ │ Detection │ Detect disappeared + returned findings │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 5. Fetch Closed│ ← Ivanti API (all closed pages) │ +│ │ Findings │ Upsert as state='closed' │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 6. BU Drift │ ← Re-query Ivanti for disappeared IDs │ +│ │ Checker │ Classify: BU reassign / severity / decom │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 7. FP Workflow │ ← Sweep closed findings for FP# tickets │ +│ │ Counts │ Aggregate by state │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 8. Anomaly │ ← Compute deltas, write to anomaly_log │ +│ │ Summary │ │ +│ └───────┬────────┘ │ +│ │ │ +│ ┌───────▼────────┐ │ +│ │ 9. Update │ ← sync_state status='success' │ +│ │ Sync State │ Notify API server: pg_notify('sync_done') │ +│ └────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Timeline Summary + +| Phase | Timeframe | Key Outcome | Required For | +|-------|-----------|-------------|--------------| +| **0** | Weeks 1–2 | Non-blocking sync, pool increase | Immediate UX fix | +| **1** | Weeks 3–4 | Collector extracted, fault isolation | Multi-team onboarding | +| **2** | Weeks 5–8 | Multi-tenancy, rate budgeting, retries | 15 teams / 100+ users | +| **3** | Weeks 9–12 | New data sources (CS/Qualys/Tanium) | Full vuln coverage | +| **4** | Weeks 13+ | Horizontal scaling, load balancing | 300+ users (if needed) | + +Phases 0–2 are recommended regardless of company-wide rollout. Phase 3 depends on data source priority decisions. Phase 4 is contingent on actual adoption numbers. + +--- + +## Next Steps + +1. Review this document and provide input on [Decision Points](#decision-points) +2. Approve Phase 0 for immediate implementation +3. Schedule Phase 1 kickoff once Phase 0 is validated in staging +4. Submit firewall requests for CrowdStrike/Qualys/Tanium access to CT107 (long lead time) diff --git a/frontend/public/index.html b/frontend/public/index.html index 7141c16..214c1df 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,7 +7,7 @@ - CVE Dashboard + AEGIS diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index a682b04..5f6c937 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "SCD", - "name": "Steam CVE Dashboard", + "short_name": "AEGIS", + "name": "AEGIS — Advanced Engineering Group Intelligence System", "icons": [ { "src": "favicon.ico", diff --git a/frontend/public/shieldlogo.jpeg b/frontend/public/shieldlogo.jpeg new file mode 100644 index 0000000..6c73df0 Binary files /dev/null and b/frontend/public/shieldlogo.jpeg differ diff --git a/frontend/src/App.js b/frontend/src/App.js index 4bc4544..66bc9bb 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1022,10 +1022,15 @@ export default function App() {
-

- STEAM Security Dashboard -

-

NTS Threat Intelligence and Metric Aggregation

+
+ AEGIS +
+

+ AEGIS +

+

Advanced Engineering Group Intelligence System

+
+
diff --git a/frontend/src/assets/shieldlogo.jpeg b/frontend/src/assets/shieldlogo.jpeg new file mode 100644 index 0000000..6c73df0 Binary files /dev/null and b/frontend/src/assets/shieldlogo.jpeg differ diff --git a/frontend/src/components/LoginForm.js b/frontend/src/components/LoginForm.js index e3b1045..f3569fd 100644 --- a/frontend/src/components/LoginForm.js +++ b/frontend/src/components/LoginForm.js @@ -30,11 +30,10 @@ export default function LoginForm() {
-
- -
-

CVE INTEL

-

Threat Intelligence Access Portal

+ {/* ⚠️ CONVENTION: Use lucide-react icons instead of tags for iconography */} + AEGIS +

AEGIS

+

Advanced Engineering Group Intelligence System

{error && ( diff --git a/frontend/src/components/NavDrawer.js b/frontend/src/components/NavDrawer.js index 4d8ba93..2807bb1 100644 --- a/frontend/src/components/NavDrawer.js +++ b/frontend/src/components/NavDrawer.js @@ -46,11 +46,16 @@ export default function NavDrawer({ isOpen, onClose, currentPage, onNavigate }) {/* Drawer header */}
-
- STEAM -
-
- Security Dashboard +
+ AEGIS +
+
+ AEGIS +
+
+ Security Dashboard +
+