fix: reclassify BU reassignment round-trips and fix backfill date-ordering bug
This commit is contained in:
@@ -62,17 +62,20 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const force = process.argv.includes('--force');
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
// Skip if already has a non-empty classification
|
// Skip if already has a non-empty classification (unless --force)
|
||||||
let existing = {};
|
if (!force) {
|
||||||
try { existing = JSON.parse(row.return_classification_json || '{}'); } catch (_) {}
|
let existing = {};
|
||||||
const hasData = Object.values(existing).some(v => v > 0);
|
try { existing = JSON.parse(row.return_classification_json || '{}'); } catch (_) {}
|
||||||
if (hasData) {
|
const hasData = Object.values(existing).some(v => v > 0);
|
||||||
skipped++;
|
if (hasData) {
|
||||||
continue;
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the date of this anomaly row
|
// Find the date of this anomaly row
|
||||||
@@ -112,13 +115,19 @@ async function main() {
|
|||||||
if (seen.has(rt.archive_id)) continue;
|
if (seen.has(rt.archive_id)) continue;
|
||||||
seen.add(rt.archive_id);
|
seen.add(rt.archive_id);
|
||||||
|
|
||||||
// Find the most recent ARCHIVED transition for this archive record
|
// Find the most recent ARCHIVED transition *before* this return
|
||||||
// (the reason it was archived before it returned)
|
// (the reason it was archived before it came back)
|
||||||
const archiveTransition = await dbGet(db,
|
const archiveTransition = await dbGet(db,
|
||||||
`SELECT reason FROM ivanti_archive_transitions
|
`SELECT reason FROM ivanti_archive_transitions
|
||||||
WHERE archive_id = ? AND to_state = 'ARCHIVED'
|
WHERE archive_id = ? AND to_state = 'ARCHIVED'
|
||||||
|
AND transitioned_at <= (
|
||||||
|
SELECT transitioned_at FROM ivanti_archive_transitions
|
||||||
|
WHERE archive_id = ? AND to_state = 'RETURNED'
|
||||||
|
AND DATE(transitioned_at) BETWEEN DATE(?, '-1 day') AND DATE(?, '+1 day')
|
||||||
|
ORDER BY transitioned_at DESC LIMIT 1
|
||||||
|
)
|
||||||
ORDER BY transitioned_at DESC LIMIT 1`,
|
ORDER BY transitioned_at DESC LIMIT 1`,
|
||||||
[rt.archive_id]
|
[rt.archive_id, rt.archive_id, date, date]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (archiveTransition && archiveTransition.reason) {
|
if (archiveTransition && archiveTransition.reason) {
|
||||||
|
|||||||
102
backend/migrations/reclassify_bu_roundtrips.js
Normal file
102
backend/migrations/reclassify_bu_roundtrips.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// reclassify_bu_roundtrips.js
|
||||||
|
//
|
||||||
|
// Reclassifies archive transitions that were part of a BU reassignment
|
||||||
|
// round-trip. These are findings that were archived (disappeared from sync)
|
||||||
|
// and then returned within a short window — indicating they were temporarily
|
||||||
|
// reassigned to a different BU and then reassigned back.
|
||||||
|
//
|
||||||
|
// The original drift checker couldn't classify these correctly because by the
|
||||||
|
// time it queried Ivanti, the findings had already been reassigned back to
|
||||||
|
// the expected BUs.
|
||||||
|
//
|
||||||
|
// After running this, re-run backfill_return_classification.js to update
|
||||||
|
// the anomaly log with the corrected reasons.
|
||||||
|
//
|
||||||
|
// Usage: node backend/migrations/reclassify_bu_roundtrips.js
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
|
||||||
|
const DB_PATH = path.join(__dirname, '..', 'cve_database.db');
|
||||||
|
|
||||||
|
// Findings that were archived and returned within this many days are
|
||||||
|
// considered BU reassignment round-trips
|
||||||
|
const ROUNDTRIP_WINDOW_DAYS = 14;
|
||||||
|
|
||||||
|
function dbAll(db, sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.all(sql, params, (err, rows) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(rows || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbRun(db, sql, params = []) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
db.run(sql, params, function (err) {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const db = new sqlite3.Database(DB_PATH);
|
||||||
|
|
||||||
|
// Find archive transitions where the finding was archived and then returned
|
||||||
|
// within the roundtrip window, and the archive reason is still the default
|
||||||
|
// severity_score_drift placeholder
|
||||||
|
const roundtrips = await dbAll(db, `
|
||||||
|
SELECT
|
||||||
|
t_arch.id AS archive_transition_id,
|
||||||
|
t_arch.archive_id,
|
||||||
|
a.finding_id,
|
||||||
|
a.finding_title,
|
||||||
|
t_arch.reason AS current_reason,
|
||||||
|
DATE(t_arch.transitioned_at) AS archived_date,
|
||||||
|
DATE(t_ret.transitioned_at) AS returned_date,
|
||||||
|
JULIANDAY(t_ret.transitioned_at) - JULIANDAY(t_arch.transitioned_at) AS days_between
|
||||||
|
FROM ivanti_archive_transitions t_arch
|
||||||
|
JOIN ivanti_finding_archives a ON a.id = t_arch.archive_id
|
||||||
|
JOIN ivanti_archive_transitions t_ret
|
||||||
|
ON t_ret.archive_id = t_arch.archive_id
|
||||||
|
AND t_ret.to_state = 'RETURNED'
|
||||||
|
AND t_ret.transitioned_at > t_arch.transitioned_at
|
||||||
|
WHERE t_arch.to_state = 'ARCHIVED'
|
||||||
|
AND t_arch.reason = 'severity_score_drift'
|
||||||
|
AND (JULIANDAY(t_ret.transitioned_at) - JULIANDAY(t_arch.transitioned_at)) BETWEEN 0 AND ?
|
||||||
|
ORDER BY t_arch.transitioned_at DESC
|
||||||
|
`, [ROUNDTRIP_WINDOW_DAYS]);
|
||||||
|
|
||||||
|
if (roundtrips.length === 0) {
|
||||||
|
console.log('No BU reassignment round-trips found to reclassify.');
|
||||||
|
db.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${roundtrips.length} archive transitions to reclassify as bu_reassignment:\n`);
|
||||||
|
|
||||||
|
let updated = 0;
|
||||||
|
for (const rt of roundtrips) {
|
||||||
|
console.log(` Finding ${rt.finding_id}: archived ${rt.archived_date}, returned ${rt.returned_date} (${Math.round(rt.days_between)}d) — ${rt.current_reason} → bu_reassignment`);
|
||||||
|
|
||||||
|
await dbRun(db,
|
||||||
|
`UPDATE ivanti_archive_transitions SET reason = 'bu_reassignment' WHERE id = ?`,
|
||||||
|
[rt.archive_transition_id]
|
||||||
|
);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nReclassified ${updated} transitions.`);
|
||||||
|
console.log('\nNow run the return classification backfill to update anomaly log rows:');
|
||||||
|
console.log(' node backend/migrations/backfill_return_classification.js');
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('Fatal error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -315,10 +315,11 @@ async function detectArchiveChanges(db, previousFindings, currentFindings) {
|
|||||||
const returnClassification = { bu_reassignment: 0, severity_drift: 0, closed_on_platform: 0, decommissioned: 0 };
|
const returnClassification = { bu_reassignment: 0, severity_drift: 0, closed_on_platform: 0, decommissioned: 0 };
|
||||||
for (const archiveId of returnedArchiveIds) {
|
for (const archiveId of returnedArchiveIds) {
|
||||||
try {
|
try {
|
||||||
// Find the most recent ARCHIVED transition reason for this archive record
|
// Find the most recent ARCHIVED transition reason *before* this return
|
||||||
const transition = await dbGet(db,
|
const transition = await dbGet(db,
|
||||||
`SELECT reason FROM ivanti_archive_transitions
|
`SELECT reason FROM ivanti_archive_transitions
|
||||||
WHERE archive_id = ? AND to_state = 'ARCHIVED'
|
WHERE archive_id = ? AND to_state = 'ARCHIVED'
|
||||||
|
AND transitioned_at <= datetime('now')
|
||||||
ORDER BY transitioned_at DESC LIMIT 1`,
|
ORDER BY transitioned_at DESC LIMIT 1`,
|
||||||
[archiveId]
|
[archiveId]
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user