Files
cve-dashboard/backend/migrations/add_closed_gone_state.js

131 lines
5.2 KiB
JavaScript
Raw Permalink Normal View History

// 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);
});