feat: consolidate setup.js with complete v1.0.0 schema — all tables, indexes, triggers for fresh deployments

This commit is contained in:
Jordan Ramos
2026-05-01 20:13:52 +00:00
parent 8e377bb85f
commit c8b3626ac5

View File

@@ -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'
}
];
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();
}
}
);
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 dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
console.log(`✓ Created directory: ${path.relative(__dirname, dir)}`);
}
}
}
// ---------------------------------------------------------------------------
// 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;
}
console.log(' Sample data added - demonstrates multi-vendor support');
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`);
}
// 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();
});
});
}
// 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();
createDirectories();
// Initialize database
const db = await initializeDatabase();
// Create default admin user
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();