From c8b3626ac5f9b24170d567aff19b80f4ac262c24 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Fri, 1 May 2026 20:13:52 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20consolidate=20setup.js=20with=20complet?= =?UTF-8?q?e=20v1.0.0=20schema=20=E2=80=94=20all=20tables,=20indexes,=20tr?= =?UTF-8?q?iggers=20for=20fresh=20deployments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/setup.js | 887 +++++++++++++++++++++++++++++++---------------- 1 file changed, 594 insertions(+), 293 deletions(-) diff --git a/backend/setup.js b/backend/setup.js index 8998753..c6688fe 100755 --- a/backend/setup.js +++ b/backend/setup.js @@ -1,5 +1,12 @@ -// Setup Script for CVE Database -// This creates a fresh database with multi-vendor support built-in +// Setup Script for CVE Dashboard v1.0.0 +// Creates a fresh database with the complete schema including all tables, +// indexes, triggers, and views needed for a new deployment. +// +// Usage: node backend/setup.js +// +// This consolidates the original schema plus all migration scripts into a +// single idempotent setup. Migration scripts in backend/migrations/ are +// retained for reference but are NOT needed on fresh deployments. const sqlite3 = require('sqlite3').verbose(); const bcrypt = require('bcryptjs'); @@ -7,334 +14,628 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); -const DB_FILE = './cve_database.db'; -const UPLOADS_DIR = './uploads'; +const DB_FILE = path.join(__dirname, 'cve_database.db'); +const UPLOADS_DIR = path.join(__dirname, 'uploads'); -// Initialize database with schema -function initializeDatabase() { +// --------------------------------------------------------------------------- +// Database helpers +// --------------------------------------------------------------------------- +function dbRun(db, sql, params = []) { return new Promise((resolve, reject) => { - const db = new sqlite3.Database(DB_FILE, (err) => { + db.run(sql, params, function (err) { if (err) reject(err); - }); - - const schema = ` - CREATE TABLE IF NOT EXISTS 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) - ); - - CREATE TABLE IF NOT EXISTS documents ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - cve_id VARCHAR(20) NOT NULL, - vendor VARCHAR(100) NOT NULL, - name VARCHAR(255) NOT NULL, - type VARCHAR(50) NOT NULL, - file_path VARCHAR(500) NOT NULL, - file_size VARCHAR(20), - mime_type VARCHAR(100), - uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - notes TEXT, - FOREIGN KEY (cve_id) REFERENCES cves(cve_id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS required_documents ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - vendor VARCHAR(100) NOT NULL, - document_type VARCHAR(50) NOT NULL, - is_mandatory BOOLEAN DEFAULT 1, - description TEXT - ); - - CREATE INDEX IF NOT EXISTS idx_cve_id ON cves(cve_id); - CREATE INDEX IF NOT EXISTS idx_vendor ON cves(vendor); - CREATE INDEX IF NOT EXISTS idx_severity ON cves(severity); - CREATE INDEX IF NOT EXISTS idx_status ON cves(status); - CREATE INDEX IF NOT EXISTS idx_doc_cve_id ON documents(cve_id); - CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor); - CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(type); - - -- Users table for authentication - CREATE TABLE IF NOT EXISTS 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')) - ); - - -- Sessions table for session management - CREATE TABLE IF NOT EXISTS 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 - ); - - CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id); - CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); - CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); - CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); - - -- Audit log table for tracking user actions - CREATE TABLE IF NOT EXISTS 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 - ); - - CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id); - CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); - CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type); - CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at); - - INSERT OR IGNORE INTO required_documents (vendor, document_type, is_mandatory, description) VALUES - ('Microsoft', 'advisory', 1, 'Official Microsoft Security Advisory'), - ('Microsoft', 'screenshot', 0, 'Proof of patch application'), - ('Cisco', 'advisory', 1, 'Cisco Security Advisory'), - ('Oracle', 'advisory', 1, 'Oracle Security Alert'), - ('VMware', 'advisory', 1, 'VMware Security Advisory'), - ('Adobe', 'advisory', 1, 'Adobe Security Bulletin'); - - CREATE VIEW IF NOT EXISTS 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; - `; - - db.exec(schema, (err) => { - if (err) { - reject(err); - } else { - console.log('✓ Database initialized successfully'); - resolve(db); - } + else resolve(this); }); }); } -// Create uploads directory structure -function createUploadsDirectory() { - if (!fs.existsSync(UPLOADS_DIR)) { - fs.mkdirSync(UPLOADS_DIR, { recursive: true }); - console.log('✓ Created uploads directory'); - } else { - console.log('✓ Uploads directory already exists'); - } -} - -// Create default admin user -async function createDefaultAdmin(db) { +function dbGet(db, sql, params = []) { return new Promise((resolve, reject) => { - // Check if admin already exists - db.get('SELECT id FROM users WHERE username = ?', ['admin'], async (err, row) => { - if (err) { - reject(err); - return; - } - - if (row) { - console.log('✓ Default admin user already exists'); - resolve(); - return; - } - - // Generate a random admin password on first run - const generatedPassword = crypto.randomBytes(12).toString('base64url'); - const passwordHash = await bcrypt.hash(generatedPassword, 10); - - db.run( - `INSERT INTO users (username, email, password_hash, role, is_active) - VALUES (?, ?, ?, ?, ?)`, - ['admin', 'admin@localhost', passwordHash, 'admin', 1], - (err) => { - if (err) { - reject(err); - } else { - console.log('✓ Created default admin user'); - console.log(`\n ╔══════════════════════════════════════════╗`); - console.log(` ║ Admin credentials (save these now!) ║`); - console.log(` ║ Username: admin ║`); - console.log(` ║ Password: ${generatedPassword.padEnd(29)}║`); - console.log(` ╚══════════════════════════════════════════╝\n`); - resolve(); - } - } - ); + db.get(sql, params, (err, row) => { + if (err) reject(err); + else resolve(row); }); }); } -// Add sample CVE data (optional - for testing) -async function addSampleData(db) { - console.log('\n📝 Adding sample CVE data for testing...'); - - const sampleCVEs = [ - { - cve_id: 'CVE-2024-SAMPLE-1', - vendor: 'Microsoft', - severity: 'Critical', - description: 'Sample remote code execution vulnerability', - published_date: '2024-01-15' - }, - { - cve_id: 'CVE-2024-SAMPLE-1', - vendor: 'Cisco', - severity: 'High', - description: 'Sample remote code execution vulnerability', - published_date: '2024-01-15' - } +function dbExec(db, sql) { + return new Promise((resolve, reject) => { + db.exec(sql, (err) => { + if (err) reject(err); + else resolve(); + }); + }); +} + +// --------------------------------------------------------------------------- +// Schema — complete v1.0.0 database structure +// --------------------------------------------------------------------------- +async function initializeDatabase(db) { + await dbExec(db, ` + + -- ================================================================= + -- Core CVE tracking tables + -- ================================================================= + + CREATE TABLE IF NOT EXISTS 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) + ); + + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + cve_id VARCHAR(20) NOT NULL, + vendor VARCHAR(100) NOT NULL, + name VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + file_path VARCHAR(500) NOT NULL, + file_size VARCHAR(20), + mime_type VARCHAR(100), + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + notes TEXT, + FOREIGN KEY (cve_id) REFERENCES cves(cve_id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS required_documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vendor VARCHAR(100) NOT NULL, + document_type VARCHAR(50) NOT NULL, + is_mandatory BOOLEAN DEFAULT 1, + description TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_cve_id ON cves(cve_id); + CREATE INDEX IF NOT EXISTS idx_vendor ON cves(vendor); + CREATE INDEX IF NOT EXISTS idx_severity ON cves(severity); + CREATE INDEX IF NOT EXISTS idx_status ON cves(status); + CREATE INDEX IF NOT EXISTS idx_doc_cve_id ON documents(cve_id); + CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor); + CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(type); + + -- ================================================================= + -- Authentication and session management + -- ================================================================= + + CREATE TABLE IF NOT EXISTS 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, + user_group VARCHAR(20) NOT NULL DEFAULT 'Read_Only', + CHECK (role IN ('admin', 'editor', 'viewer')) + ); + + CREATE TABLE IF NOT EXISTS 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 + ); + + CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id); + CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); + CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at); + CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); + CREATE INDEX IF NOT EXISTS idx_users_user_group ON users(user_group); + + -- ================================================================= + -- Audit logging + -- ================================================================= + + CREATE TABLE IF NOT EXISTS 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 + ); + + CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id); + CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); + CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type); + CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at); + + -- ================================================================= + -- Jira integration + -- ================================================================= + + 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 + ); + + CREATE INDEX IF NOT EXISTS idx_jira_tickets_cve ON jira_tickets(cve_id, vendor); + CREATE INDEX IF NOT EXISTS idx_jira_tickets_status ON jira_tickets(status); + + -- ================================================================= + -- Archer integration + -- ================================================================= + + CREATE TABLE IF NOT EXISTS archer_tickets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + exc_number TEXT NOT NULL UNIQUE, + archer_url TEXT, + status TEXT DEFAULT 'Draft' CHECK(status IN ('Draft', 'Open', 'Under Review', 'Accepted')), + cve_id TEXT NOT NULL, + vendor TEXT NOT NULL, + 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 + ); + + CREATE INDEX IF NOT EXISTS idx_archer_tickets_cve ON archer_tickets(cve_id, vendor); + CREATE INDEX IF NOT EXISTS idx_archer_tickets_status ON archer_tickets(status); + CREATE INDEX IF NOT EXISTS idx_archer_tickets_exc ON archer_tickets(exc_number); + + -- ================================================================= + -- Knowledge base + -- ================================================================= + + CREATE TABLE IF NOT EXISTS knowledge_base ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE NOT NULL, + description TEXT, + category VARCHAR(100), + file_path VARCHAR(500), + file_name VARCHAR(255), + file_type VARCHAR(50), + file_size INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by INTEGER, + FOREIGN KEY (created_by) REFERENCES users(id) + ); + + CREATE INDEX IF NOT EXISTS idx_knowledge_base_slug ON knowledge_base(slug); + CREATE INDEX IF NOT EXISTS idx_knowledge_base_category ON knowledge_base(category); + CREATE INDEX IF NOT EXISTS idx_knowledge_base_created_at ON knowledge_base(created_at DESC); + + -- ================================================================= + -- Ivanti findings sync and cache + -- ================================================================= + + CREATE TABLE IF NOT EXISTS ivanti_sync_state ( + id INTEGER PRIMARY KEY CHECK (id = 1), + total INTEGER DEFAULT 0, + workflows_json TEXT DEFAULT '[]', + synced_at DATETIME, + sync_status TEXT DEFAULT 'never', + error_message TEXT + ); + + CREATE TABLE IF NOT EXISTS ivanti_findings_cache ( + id INTEGER PRIMARY KEY CHECK (id = 1), + total INTEGER DEFAULT 0, + findings_json TEXT DEFAULT '[]', + synced_at DATETIME, + sync_status TEXT DEFAULT 'never', + error_message TEXT + ); + + CREATE TABLE IF NOT EXISTS ivanti_finding_notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + finding_id TEXT NOT NULL UNIQUE, + note TEXT NOT NULL DEFAULT '', + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_finding_notes_finding_id ON ivanti_finding_notes(finding_id); + + CREATE TABLE IF NOT EXISTS ivanti_counts_cache ( + id INTEGER PRIMARY KEY CHECK (id = 1), + open_count INTEGER DEFAULT 0, + closed_count INTEGER DEFAULT 0, + synced_at DATETIME, + fp_workflow_counts_json TEXT DEFAULT '{}', + fp_id_counts_json TEXT DEFAULT '{}' + ); + + CREATE TABLE IF NOT EXISTS ivanti_finding_overrides ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + finding_id TEXT NOT NULL, + field TEXT NOT NULL, + value TEXT NOT NULL DEFAULT '', + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE(finding_id, field) + ); + + CREATE INDEX IF NOT EXISTS idx_finding_overrides_finding_id ON ivanti_finding_overrides(finding_id); + + CREATE TABLE IF NOT EXISTS ivanti_counts_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + open_count INTEGER NOT NULL, + closed_count INTEGER NOT NULL, + recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + -- ================================================================= + -- Ivanti FP (False Positive) submissions + -- ================================================================= + + CREATE TABLE IF NOT EXISTS ivanti_fp_submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + username TEXT NOT NULL, + ivanti_workflow_batch_id INTEGER, + ivanti_generated_id TEXT, + ivanti_workflow_batch_uuid TEXT, + workflow_name TEXT NOT NULL, + reason TEXT NOT NULL, + description TEXT, + expiration_date TEXT NOT NULL, + scope_override TEXT NOT NULL DEFAULT 'Authorized', + finding_ids_json TEXT NOT NULL, + queue_item_ids_json TEXT NOT NULL, + attachment_count INTEGER DEFAULT 0, + attachment_results_json TEXT, + status TEXT NOT NULL DEFAULT 'success' CHECK(status IN ('success', 'partial', 'failed')), + lifecycle_status TEXT NOT NULL DEFAULT 'submitted' CHECK(lifecycle_status IN ('submitted', 'approved', 'rejected', 'rework', 'resubmitted')), + error_message TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_fp_submissions_user ON ivanti_fp_submissions(user_id); + CREATE INDEX IF NOT EXISTS idx_fp_submissions_ivanti_id ON ivanti_fp_submissions(ivanti_generated_id); + + CREATE TABLE IF NOT EXISTS ivanti_fp_submission_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + submission_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + username TEXT NOT NULL, + change_type TEXT NOT NULL CHECK(change_type IN ( + 'created', 'fields_updated', 'findings_added', + 'attachments_added', 'status_changed' + )), + change_details_json TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (submission_id) REFERENCES ivanti_fp_submissions(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_fp_history_submission ON ivanti_fp_submission_history(submission_id); + + -- ================================================================= + -- Ivanti todo queue (FP, Archer, CARD, GRANITE workflows) + -- ================================================================= + + CREATE TABLE IF NOT EXISTS ivanti_todo_queue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + finding_id TEXT NOT NULL, + finding_title TEXT, + cves_json TEXT, + ip_address TEXT, + hostname TEXT, + vendor TEXT NOT NULL, + workflow_type TEXT NOT NULL CHECK(workflow_type IN ('FP', 'Archer', 'CARD', 'GRANITE')), + status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'complete')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_todo_queue_user ON ivanti_todo_queue(user_id, status); + + -- ================================================================= + -- Ivanti archive detection and anomaly tracking + -- ================================================================= + + CREATE TABLE IF NOT EXISTS ivanti_finding_archives ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + finding_id TEXT NOT NULL UNIQUE, + finding_title TEXT NOT NULL DEFAULT '', + host_name TEXT NOT NULL DEFAULT '', + ip_address TEXT NOT NULL DEFAULT '', + current_state TEXT NOT NULL CHECK(current_state IN ('ARCHIVED','RETURNED','CLOSED','CLOSED_GONE')), + last_severity REAL NOT NULL DEFAULT 0, + first_archived_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_transition_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_archive_finding_id ON ivanti_finding_archives(finding_id); + CREATE INDEX IF NOT EXISTS idx_archive_current_state ON ivanti_finding_archives(current_state); + + CREATE TABLE IF NOT EXISTS ivanti_archive_transitions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + archive_id INTEGER NOT NULL, + from_state TEXT NOT NULL, + to_state TEXT NOT NULL, + severity_at_transition REAL NOT NULL DEFAULT 0, + reason TEXT NOT NULL DEFAULT '', + transitioned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (archive_id) REFERENCES ivanti_finding_archives(id) + ); + + CREATE INDEX IF NOT EXISTS idx_transition_archive_id ON ivanti_archive_transitions(archive_id); + + CREATE TABLE IF NOT EXISTS ivanti_sync_anomaly_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sync_timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + open_count_delta INTEGER NOT NULL DEFAULT 0, + closed_count_delta INTEGER NOT NULL DEFAULT 0, + newly_archived_count INTEGER NOT NULL DEFAULT 0, + returned_count INTEGER NOT NULL DEFAULT 0, + classification_json TEXT NOT NULL DEFAULT '{}', + return_classification_json TEXT NOT NULL DEFAULT '{}', + is_significant INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_anomaly_sync_timestamp ON ivanti_sync_anomaly_log(sync_timestamp); + + CREATE TABLE IF NOT EXISTS ivanti_finding_bu_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + finding_id TEXT NOT NULL, + finding_title TEXT NOT NULL DEFAULT '', + host_name TEXT NOT NULL DEFAULT '', + previous_bu TEXT NOT NULL, + new_bu TEXT NOT NULL, + detected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_bu_history_finding_id ON ivanti_finding_bu_history(finding_id); + CREATE INDEX IF NOT EXISTS idx_bu_history_detected_at ON ivanti_finding_bu_history(detected_at); + + -- ================================================================= + -- Atlas action plans cache + -- ================================================================= + + CREATE TABLE IF NOT EXISTS atlas_action_plans_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL UNIQUE, + has_action_plan INTEGER NOT NULL DEFAULT 0, + plan_count INTEGER NOT NULL DEFAULT 0, + plans_json TEXT NOT NULL DEFAULT '[]', + synced_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_atlas_cache_host_id ON atlas_action_plans_cache(host_id); + + -- ================================================================= + -- Compliance (NTS AEO) tracking + -- ================================================================= + + CREATE TABLE IF NOT EXISTS compliance_uploads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + filename TEXT NOT NULL, + report_date TEXT, + uploaded_by INTEGER, + uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP, + new_count INTEGER DEFAULT 0, + resolved_count INTEGER DEFAULT 0, + recurring_count INTEGER DEFAULT 0, + summary_json TEXT, + FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS compliance_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + upload_id INTEGER NOT NULL, + hostname TEXT NOT NULL, + ip_address TEXT, + device_type TEXT, + team TEXT, + metric_id TEXT NOT NULL, + metric_desc TEXT, + category TEXT, + extra_json TEXT, + status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'resolved')), + first_seen_upload_id INTEGER, + resolved_upload_id INTEGER, + seen_count INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (upload_id) REFERENCES compliance_uploads(id) ON DELETE CASCADE, + FOREIGN KEY (first_seen_upload_id) REFERENCES compliance_uploads(id) ON DELETE SET NULL, + FOREIGN KEY (resolved_upload_id) REFERENCES compliance_uploads(id) ON DELETE SET NULL + ); + + CREATE INDEX IF NOT EXISTS idx_compliance_items_upload ON compliance_items(upload_id); + CREATE INDEX IF NOT EXISTS idx_compliance_items_identity ON compliance_items(hostname, metric_id); + CREATE INDEX IF NOT EXISTS idx_compliance_items_team_status ON compliance_items(team, status); + + CREATE TABLE IF NOT EXISTS compliance_notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hostname TEXT NOT NULL, + metric_id TEXT NOT NULL, + note TEXT NOT NULL, + group_id TEXT, + created_by INTEGER, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL + ); + + CREATE INDEX IF NOT EXISTS idx_compliance_notes_identity ON compliance_notes(hostname, metric_id); + CREATE INDEX IF NOT EXISTS idx_compliance_notes_group ON compliance_notes(group_id); + + -- ================================================================= + -- Document compliance view + -- ================================================================= + + CREATE VIEW IF NOT EXISTS 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; + + -- ================================================================= + -- Seed data + -- ================================================================= + + INSERT OR IGNORE INTO required_documents (vendor, document_type, is_mandatory, description) VALUES + ('Microsoft', 'advisory', 1, 'Official Microsoft Security Advisory'), + ('Microsoft', 'screenshot', 0, 'Proof of patch application'), + ('Cisco', 'advisory', 1, 'Cisco Security Advisory'), + ('Oracle', 'advisory', 1, 'Oracle Security Alert'), + ('VMware', 'advisory', 1, 'VMware Security Advisory'), + ('Adobe', 'advisory', 1, 'Adobe Security Bulletin'); + `); + + console.log('✓ Database schema initialized'); + + // User group validation triggers (cannot be in db.exec multi-statement) + await dbRun(db, ` + CREATE TRIGGER IF NOT EXISTS check_user_group_insert + BEFORE INSERT ON users + FOR EACH ROW + WHEN NEW.user_group NOT IN ('Admin', 'Standard_User', 'Leadership', 'Read_Only') + BEGIN + SELECT RAISE(ABORT, 'Invalid user_group value. Must be Admin, Standard_User, Leadership, or Read_Only'); + END + `); + + await dbRun(db, ` + CREATE TRIGGER IF NOT EXISTS check_user_group_update + BEFORE UPDATE OF user_group ON users + FOR EACH ROW + WHEN NEW.user_group NOT IN ('Admin', 'Standard_User', 'Leadership', 'Read_Only') + BEGIN + SELECT RAISE(ABORT, 'Invalid user_group value. Must be Admin, Standard_User, Leadership, or Read_Only'); + END + `); + + console.log('✓ Triggers created'); +} + +// --------------------------------------------------------------------------- +// Directory setup +// --------------------------------------------------------------------------- +function createDirectories() { + const dirs = [ + UPLOADS_DIR, + path.join(UPLOADS_DIR, 'temp'), + path.join(UPLOADS_DIR, 'knowledge_base'), ]; - - for (const cve of sampleCVEs) { - await new Promise((resolve, reject) => { - db.run( - `INSERT OR IGNORE INTO cves (cve_id, vendor, severity, description, published_date) - VALUES (?, ?, ?, ?, ?)`, - [cve.cve_id, cve.vendor, cve.severity, cve.description, cve.published_date], - (err) => { - if (err) reject(err); - else { - console.log(` ✓ Added sample: ${cve.cve_id} / ${cve.vendor}`); - resolve(); - } - } - ); - }); + for (const dir of dirs) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + console.log(`✓ Created directory: ${path.relative(__dirname, dir)}`); + } } - - console.log('ℹ️ Sample data added - demonstrates multi-vendor support'); } -// Verify database structure -async function verifySetup(db) { - return new Promise((resolve) => { - db.get('SELECT sql FROM sqlite_master WHERE type="table" AND name="cves"', (err, row) => { - if (err) { - console.error('Warning: Could not verify setup:', err); - } else { - console.log('\n📋 CVEs table structure:'); - console.log(row.sql); - - // Check if UNIQUE constraint is correct - if (row.sql.includes('UNIQUE(cve_id, vendor)')) { - console.log('\n✅ Multi-vendor support: ENABLED'); - } else { - console.log('\n⚠️ Warning: Multi-vendor constraint may not be set correctly'); - } - } - resolve(); - }); - }); +// --------------------------------------------------------------------------- +// Default admin user +// --------------------------------------------------------------------------- +async function createDefaultAdmin(db) { + const existing = await dbGet(db, 'SELECT id FROM users WHERE username = ?', ['admin']); + if (existing) { + console.log('✓ Default admin user already exists'); + return; + } + + const generatedPassword = crypto.randomBytes(12).toString('base64url'); + const passwordHash = await bcrypt.hash(generatedPassword, 10); + + await dbRun(db, + `INSERT INTO users (username, email, password_hash, role, user_group, is_active) + VALUES (?, ?, ?, ?, ?, ?)`, + ['admin', 'admin@localhost', passwordHash, 'admin', 'Admin', 1] + ); + + console.log('✓ Created default admin user'); + console.log(`\n ╔══════════════════════════════════════════╗`); + console.log(` ║ Admin credentials (save these now!) ║`); + console.log(` ║ Username: admin ║`); + console.log(` ║ Password: ${generatedPassword.padEnd(29)}║`); + console.log(` ╚══════════════════════════════════════════╝\n`); } -// Display setup summary +// --------------------------------------------------------------------------- +// Setup summary +// --------------------------------------------------------------------------- function displaySummary() { console.log('\n╔════════════════════════════════════════════════════════╗'); - console.log('║ CVE DATABASE SETUP COMPLETE! ║'); + console.log('║ CVE DASHBOARD v1.0.0 — SETUP COMPLETE ║'); console.log('╚════════════════════════════════════════════════════════╝'); - console.log('\n📊 What was created:'); - console.log(' ✓ SQLite database (cve_database.db)'); - console.log(' ✓ Tables: cves, documents, required_documents, users, sessions, audit_logs'); - console.log(' ✓ Multi-vendor support with UNIQUE(cve_id, vendor)'); - console.log(' ✓ Vendor column in documents table'); - console.log(' ✓ User authentication with session-based auth'); - console.log(' ✓ Indexes for fast queries'); - console.log(' ✓ Document compliance view'); - console.log(' ✓ Uploads directory for file storage'); - console.log(' ✓ Default admin user (see credentials above)'); - console.log('\n📁 File structure will be:'); - console.log(' uploads/'); - console.log(' └── CVE-XXXX-XXXX/'); - console.log(' ├── Vendor1/'); - console.log(' │ ├── advisory.pdf'); - console.log(' │ └── screenshot.png'); - console.log(' └── Vendor2/'); - console.log(' └── advisory.pdf'); + console.log('\n📊 Tables created:'); + console.log(' Core: cves, documents, required_documents'); + console.log(' Auth: users, sessions'); + console.log(' Audit: audit_logs'); + console.log(' Jira: jira_tickets'); + console.log(' Archer: archer_tickets'); + console.log(' KB: knowledge_base'); + console.log(' Ivanti: ivanti_sync_state, ivanti_findings_cache,'); + console.log(' ivanti_finding_notes, ivanti_counts_cache,'); + console.log(' ivanti_finding_overrides, ivanti_counts_history,'); + console.log(' ivanti_fp_submissions, ivanti_fp_submission_history,'); + console.log(' ivanti_todo_queue'); + console.log(' Archives: ivanti_finding_archives, ivanti_archive_transitions,'); + console.log(' ivanti_sync_anomaly_log, ivanti_finding_bu_history'); + console.log(' Atlas: atlas_action_plans_cache'); + console.log(' Compliance: compliance_uploads, compliance_items, compliance_notes'); console.log('\n🚀 Next steps:'); - console.log(' 1. Start the backend API:'); - console.log(' → cd backend && node server.js'); - console.log(' 2. Start the frontend:'); - console.log(' → cd frontend && npm start'); - console.log(' 3. Open http://localhost:3000'); - console.log(' 4. Start adding CVEs with multiple vendors!'); - console.log('\n💡 Key Features:'); - console.log(' • Add same CVE-ID with different vendors'); - console.log(' • Each vendor has separate document storage'); - console.log(' • Quick Check shows all vendors for a CVE'); - console.log(' • Document compliance tracking per vendor'); - console.log(' • Required docs: Advisory (mandatory for most vendors)\n'); + console.log(' 1. Copy .env.example to .env and configure API keys'); + console.log(' 2. Start the backend: node backend/server.js'); + console.log(' 3. Build the frontend: cd frontend && npm run build'); + console.log(' 4. Open the dashboard and log in with the admin credentials above\n'); } -// Main execution +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- async function main() { - console.log('🚀 CVE Database Setup (Multi-Vendor Support)\n'); + console.log('🚀 CVE Dashboard v1.0.0 — Database Setup\n'); console.log('════════════════════════════════════════\n'); - - try { - // Create uploads directory - createUploadsDirectory(); - - // Initialize database - const db = await initializeDatabase(); - // Create default admin user + try { + createDirectories(); + + const db = new sqlite3.Database(DB_FILE); + await initializeDatabase(db); await createDefaultAdmin(db); - // Add sample data - await addSampleData(db); - - // Verify setup - await verifySetup(db); - - // Close database connection db.close((err) => { if (err) console.error('Error closing database:', err); - else console.log('\n✓ Database connection closed'); - - // Display summary + else console.log('✓ Database connection closed'); displaySummary(); }); - } catch (error) { console.error('❌ Setup Error:', error); process.exit(1); } } -// Run the setup main();