Add sync anomaly detection, BU drift monitoring, and findings count investigation
- Add BU drift checker that classifies archived findings as BU reassignment, severity drift, closure, or decommission via unfiltered Ivanti API queries - Add post-sync anomaly summary with significance threshold and classification breakdown stored in ivanti_sync_anomaly_log table - Add per-finding BU tracking that detects BU changes across syncs and records them in ivanti_finding_bu_history table - Add drift guard that skips trend history writes when total drops more than 50% - Add CLOSED_GONE archive state for findings that vanish from the closed set - Add anomaly banner UI on Vulnerability Triage page for significant sync changes - Add API endpoints for anomaly latest/history and BU change tracking - Add diagnostic scripts for drift checking and BU reassignment verification - Add investigation document and xlsx export for the April 2026 BU reassignment incident where 109 findings were moved to SDIT-CSD-ITLS-PIES - Migrations required: add_closed_gone_state.js, add_sync_anomaly_tables.js
This commit is contained in:
90
backend/migrations/add_sync_anomaly_tables.js
Normal file
90
backend/migrations/add_sync_anomaly_tables.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// Migration: Add sync anomaly detection and BU drift monitoring tables
|
||||
//
|
||||
// Creates two new tables:
|
||||
// - ivanti_sync_anomaly_log — stores one row per sync cycle with the
|
||||
// anomaly summary breakdown (count deltas, classification, significance).
|
||||
// - ivanti_finding_bu_history — records BU change events detected on
|
||||
// individual findings across syncs.
|
||||
//
|
||||
// Safe to re-run — uses CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS.
|
||||
//
|
||||
// Usage: node backend/migrations/add_sync_anomaly_tables.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 sync anomaly tables 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() {
|
||||
// 1. Create ivanti_sync_anomaly_log table
|
||||
await run(`
|
||||
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 '{}',
|
||||
is_significant INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
console.log('✓ ivanti_sync_anomaly_log table ready');
|
||||
|
||||
// 2. Create ivanti_finding_bu_history table
|
||||
await run(`
|
||||
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
|
||||
)
|
||||
`);
|
||||
console.log('✓ ivanti_finding_bu_history table ready');
|
||||
|
||||
// 3. Create indexes
|
||||
await run('CREATE INDEX IF NOT EXISTS idx_anomaly_sync_timestamp ON ivanti_sync_anomaly_log(sync_timestamp)');
|
||||
console.log('✓ idx_anomaly_sync_timestamp index ready');
|
||||
|
||||
await run('CREATE INDEX IF NOT EXISTS idx_bu_history_finding_id ON ivanti_finding_bu_history(finding_id)');
|
||||
console.log('✓ idx_bu_history_finding_id index ready');
|
||||
|
||||
await run('CREATE INDEX IF NOT EXISTS idx_bu_history_detected_at ON ivanti_finding_bu_history(detected_at)');
|
||||
console.log('✓ idx_bu_history_detected_at index ready');
|
||||
}
|
||||
|
||||
migrate()
|
||||
.then(() => {
|
||||
console.log('Migration complete.');
|
||||
db.close();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Migration failed:', err);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user