// Migration: Add CLOSED_GONE state to ivanti_finding_archives // // The archive table tracks findings that disappear from the Open findings set. // Previously it only tracked: ARCHIVED → RETURNED → CLOSED. // // This migration adds a CLOSED_GONE state for findings that were confirmed // in the Ivanti Closed set but then disappeared from it on a subsequent sync. // This closes a visibility gap where findings could vanish from the Closed API // results (e.g., due to VRR rescore below the severity threshold) without // being tracked. // // SQLite does not support ALTER TABLE to modify CHECK constraints, so this // migration recreates the table with the expanded constraint. // // Safe to re-run — uses IF NOT EXISTS and checks for existing data. // // Usage: node backend/migrations/add_closed_gone_state.js const path = require('path'); const sqlite3 = require('sqlite3').verbose(); const dbPath = path.join(__dirname, '..', 'cve_database.db'); const db = new sqlite3.Database(dbPath); console.log('Starting CLOSED_GONE state migration...'); function run(sql, params = []) { return new Promise((resolve, reject) => { db.run(sql, params, function (err) { if (err) reject(err); else resolve(this); }); }); } function all(sql, params = []) { return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); } async function migrate() { // Check if the table already has the CLOSED_GONE state const tableInfo = await all("SELECT sql FROM sqlite_master WHERE name='ivanti_finding_archives'"); if (tableInfo.length > 0 && tableInfo[0].sql.includes('CLOSED_GONE')) { console.log('✓ ivanti_finding_archives already has CLOSED_GONE state — skipping'); return; } if (tableInfo.length === 0) { // Table doesn't exist yet — create it fresh with the new constraint await run(` CREATE TABLE 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 ) `); console.log('✓ Created ivanti_finding_archives with CLOSED_GONE state'); return; } // Table exists but needs the constraint updated — recreate with data migration console.log(' Recreating table with expanded CHECK constraint...'); await run('BEGIN TRANSACTION'); try { // 1. Rename existing table await run('ALTER TABLE ivanti_finding_archives RENAME TO ivanti_finding_archives_old'); // 2. Create new table with expanded constraint await run(` CREATE TABLE 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 ) `); // 3. Copy data await run(` INSERT INTO ivanti_finding_archives (id, finding_id, finding_title, host_name, ip_address, current_state, last_severity, first_archived_at, last_transition_at, created_at) SELECT id, finding_id, finding_title, host_name, ip_address, current_state, last_severity, first_archived_at, last_transition_at, created_at FROM ivanti_finding_archives_old `); // 4. Recreate indexes await run('CREATE INDEX IF NOT EXISTS idx_archive_finding_id ON ivanti_finding_archives(finding_id)'); await run('CREATE INDEX IF NOT EXISTS idx_archive_current_state ON ivanti_finding_archives(current_state)'); // 5. Drop old table await run('DROP TABLE ivanti_finding_archives_old'); await run('COMMIT'); console.log('✓ ivanti_finding_archives updated with CLOSED_GONE state'); } catch (err) { await run('ROLLBACK').catch(() => {}); throw err; } } migrate() .then(() => { console.log('Migration complete.'); db.close(); }) .catch((err) => { console.error('Migration failed:', err); db.close(); process.exit(1); });