642 lines
28 KiB
JavaScript
Executable File
642 lines
28 KiB
JavaScript
Executable File
// 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');
|
|
const crypto = require('crypto');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const DB_FILE = path.join(__dirname, 'cve_database.db');
|
|
const UPLOADS_DIR = path.join(__dirname, 'uploads');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Database helpers
|
|
// ---------------------------------------------------------------------------
|
|
function dbRun(db, sql, params = []) {
|
|
return new Promise((resolve, reject) => {
|
|
db.run(sql, params, function (err) {
|
|
if (err) reject(err);
|
|
else resolve(this);
|
|
});
|
|
});
|
|
}
|
|
|
|
function dbGet(db, sql, params = []) {
|
|
return new Promise((resolve, reject) => {
|
|
db.get(sql, params, (err, row) => {
|
|
if (err) reject(err);
|
|
else resolve(row);
|
|
});
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Setup summary
|
|
// ---------------------------------------------------------------------------
|
|
function displaySummary() {
|
|
console.log('\n╔════════════════════════════════════════════════════════╗');
|
|
console.log('║ CVE DASHBOARD v1.0.0 — SETUP COMPLETE ║');
|
|
console.log('╚════════════════════════════════════════════════════════╝');
|
|
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. 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
|
|
// ---------------------------------------------------------------------------
|
|
async function main() {
|
|
console.log('🚀 CVE Dashboard v1.0.0 — Database Setup\n');
|
|
console.log('════════════════════════════════════════\n');
|
|
|
|
try {
|
|
createDirectories();
|
|
|
|
const db = new sqlite3.Database(DB_FILE);
|
|
await initializeDatabase(db);
|
|
await createDefaultAdmin(db);
|
|
|
|
db.close((err) => {
|
|
if (err) console.error('Error closing database:', err);
|
|
else console.log('✓ Database connection closed');
|
|
displaySummary();
|
|
});
|
|
} catch (error) {
|
|
console.error('❌ Setup Error:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|