diff --git a/.gitignore b/.gitignore index db42580..e6bd1bc 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,11 @@ backend/setup.js-backup # Kiro implementation summary (internal only) docs/kiro-implementation-summary.md + +# Diagnostic scripts (troubleshooting only) +backend/scripts/drift-check.js +backend/scripts/bu-reassignment-check.js +backend/scripts/export-reassigned-findings.js + +# Investigation exports +docs/reassigned-findings-*.xlsx diff --git a/backend/scripts/bu-reassignment-check.js b/backend/scripts/bu-reassignment-check.js deleted file mode 100644 index 99e5273..0000000 --- a/backend/scripts/bu-reassignment-check.js +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env node -// bu-reassignment-check.js — Check if disappeared findings were reassigned to a different BU -// -// Queries Ivanti for the specific finding IDs that are completely gone from our -// BU-filtered results, using NO filters at all (just the finding IDs). -// If they come back with a different BU, that confirms BU reassignment. -// -// Usage: node backend/scripts/bu-reassignment-check.js - -require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); - -const path = require('path'); -const sqlite3 = require('sqlite3').verbose(); -const { ivantiPost } = require('../helpers/ivantiApi'); - -const DB_PATH = path.join(__dirname, '..', 'cve_database.db'); - -function dbAll(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.all(sql, params, (err, rows) => { - if (err) reject(err); - else resolve(rows || []); - }); - }); -} - -async function queryByFindingIds(findingIds, apiKey, clientId, skipTls) { - const urlPath = `/client/${encodeURIComponent(clientId)}/hostFinding/search`; - const allResults = []; - - // Ivanti's IN filter can handle batches — but let's chunk to be safe - const chunkSize = 50; - for (let i = 0; i < findingIds.length; i += chunkSize) { - const chunk = findingIds.slice(i, i + chunkSize); - const idList = chunk.join(','); - - // Query with ONLY the finding ID filter — no BU, no severity, no state - const filters = [ - { - field: 'id', - exclusive: false, - operator: 'IN', - orWithPrevious: false, - implicitFilters: [], - value: idList, - caseSensitive: false - } - ]; - - let page = 0; - let totalPages = 1; - - do { - const body = { - filters, - projection: 'internal', - sort: [{ field: 'severity', direction: 'ASC' }], - page, - size: 100 - }; - - try { - const result = await ivantiPost(urlPath, body, apiKey, skipTls); - if (result.status !== 200) { - console.error(` API returned status ${result.status} for chunk starting at ${i}`); - break; - } - - const data = JSON.parse(result.body); - totalPages = data.page?.totalPages || 1; - const findings = data._embedded?.hostFindings || []; - - for (const f of findings) { - const bu = f.assetCustomAttributes?.['1550_host_1']?.[0] || 'UNKNOWN'; - allResults.push({ - id: String(f.id), - severity: typeof f.severity === 'number' ? f.severity : parseFloat(f.severity) || 0, - title: f.title || '', - hostName: f.host?.hostName || '', - ipAddress: f.host?.ipAddress || '', - state: f.status || f.generic_state || '', - bu, - // Check for FP workflow - fpWorkflow: extractFP(f) - }); - } - - console.error(` Chunk ${Math.floor(i/chunkSize)+1}: page ${page+1}/${totalPages}, ${findings.length} results`); - page++; - } catch (err) { - console.error(` Error querying chunk at ${i}:`, err.message); - break; - } - } while (page < totalPages); - } - - return allResults; -} - -function extractFP(f) { - const wfDist = f.workflowDistribution || {}; - const fpBuckets = [ - ...(wfDist.approvedWorkflows || []), - ...(wfDist.actionableWorkflows || []), - ...(wfDist.requestedWorkflows || []), - ...(wfDist.reworkedWorkflows || []), - ...(wfDist.rejectedWorkflows || []), - ...(wfDist.expiredWorkflows || []), - ].filter(w => (w.generatedId || '').startsWith('FP#')); - const entry = fpBuckets[0]; - if (!entry) return null; - return { id: entry.generatedId, state: entry.state }; -} - -async function main() { - const apiKey = process.env.IVANTI_API_KEY; - const clientId = process.env.IVANTI_CLIENT_ID || '1550'; - const skipTls = process.env.IVANTI_SKIP_TLS === 'true'; - - if (!apiKey) { - console.error('IVANTI_API_KEY not set'); - process.exit(1); - } - - const db = new sqlite3.Database(DB_PATH); - - // Get the 124 finding IDs that were completely gone from BU-filtered results - const goneFindings = await dbAll(db, - `SELECT finding_id, last_severity, finding_title, current_state - FROM ivanti_finding_archives - WHERE current_state IN ('ARCHIVED', 'CLOSED')` - ); - - const goneIds = goneFindings.map(f => f.finding_id); - console.error(`\n=== BU Reassignment Check ===`); - console.error(`Querying Ivanti for ${goneIds.length} disappeared finding IDs (no BU/severity/state filter)...\n`); - - const results = await queryByFindingIds(goneIds, apiKey, clientId, skipTls); - - const foundMap = new Map(results.map(r => [r.id, r])); - - // Categorize - const reassigned = []; // Found with different BU - const sameBU = []; // Found with same BU (STEAM or ACCESS-ENG) - const notFound = []; // Still not found even without filters - const withFP = []; // Has an FP workflow (any state) - - for (const arch of goneFindings) { - const found = foundMap.get(arch.finding_id); - if (!found) { - notFound.push(arch); - } else if (found.bu !== 'NTS-AEO-ACCESS-ENG' && found.bu !== 'NTS-AEO-STEAM') { - reassigned.push({ ...arch, currentBU: found.bu, currentSeverity: found.severity, currentState: found.state, fp: found.fpWorkflow }); - if (found.fpWorkflow) withFP.push({ ...arch, ...found }); - } else { - sameBU.push({ ...arch, currentBU: found.bu, currentSeverity: found.severity, currentState: found.state, fp: found.fpWorkflow }); - if (found.fpWorkflow) withFP.push({ ...arch, ...found }); - } - } - - console.log(''); - console.log('='.repeat(130)); - console.log('BU REASSIGNMENT CHECK RESULTS'); - console.log('='.repeat(130)); - - console.log(`\nREASSIGNED TO DIFFERENT BU: ${reassigned.length} findings`); - console.log('-'.repeat(130)); - if (reassigned.length > 0) { - console.log( - 'Finding ID'.padEnd(14) + - 'Archive State'.padEnd(15) + - 'Last Sev'.padEnd(10) + - 'Current Sev'.padEnd(13) + - 'Current BU'.padEnd(30) + - 'FP Workflow'.padEnd(25) + - 'Title' - ); - console.log('-'.repeat(130)); - for (const f of reassigned) { - const fpStr = f.fp ? `${f.fp.id} (${f.fp.state})` : '-'; - console.log( - f.finding_id.padEnd(14) + - f.current_state.padEnd(15) + - f.last_severity.toFixed(2).padEnd(10) + - f.currentSeverity.toFixed(2).padEnd(13) + - f.currentBU.padEnd(30) + - fpStr.padEnd(25) + - f.finding_title.substring(0, 40) - ); - } - } - - console.log(`\nSTILL SAME BU (but missing from filtered results): ${sameBU.length} findings`); - console.log('-'.repeat(130)); - if (sameBU.length > 0) { - console.log( - 'Finding ID'.padEnd(14) + - 'Archive State'.padEnd(15) + - 'Last Sev'.padEnd(10) + - 'Current Sev'.padEnd(13) + - 'Current BU'.padEnd(30) + - 'Current State'.padEnd(15) + - 'Title' - ); - console.log('-'.repeat(130)); - for (const f of sameBU) { - console.log( - f.finding_id.padEnd(14) + - f.current_state.padEnd(15) + - f.last_severity.toFixed(2).padEnd(10) + - f.currentSeverity.toFixed(2).padEnd(13) + - f.currentBU.padEnd(30) + - f.currentState.padEnd(15) + - f.finding_title.substring(0, 40) - ); - } - } - - console.log(`\nCOMPLETELY GONE (not found even without any filters): ${notFound.length} findings`); - if (notFound.length > 0 && notFound.length <= 20) { - console.log('-'.repeat(130)); - for (const f of notFound) { - console.log(` ${f.finding_id} ${f.last_severity.toFixed(2)} ${f.finding_title.substring(0, 60)}`); - } - } - - if (withFP.length > 0) { - console.log(`\nFINDINGS WITH FP WORKFLOWS: ${withFP.length}`); - console.log('-'.repeat(130)); - for (const f of withFP) { - const fpStr = f.fpWorkflow ? `${f.fpWorkflow.id} (${f.fpWorkflow.state})` : f.fp ? `${f.fp.id} (${f.fp.state})` : '-'; - console.log(` ${f.finding_id || f.id} ${fpStr} ${f.bu || f.currentBU} ${(f.finding_title || f.title || '').substring(0, 50)}`); - } - } - - // Summary - console.log(''); - console.log('='.repeat(130)); - console.log('SUMMARY'); - console.log('='.repeat(130)); - console.log(` Total disappeared findings checked: ${goneFindings.length}`); - console.log(` Reassigned to different BU: ${reassigned.length}`); - console.log(` Still same BU (unexpected): ${sameBU.length}`); - console.log(` Completely gone from platform: ${notFound.length}`); - console.log(` Have FP workflows: ${withFP.length}`); - - if (reassigned.length > 0) { - const buCounts = {}; - reassigned.forEach(f => { buCounts[f.currentBU] = (buCounts[f.currentBU] || 0) + 1; }); - console.log('\n BU reassignment breakdown:'); - for (const [bu, cnt] of Object.entries(buCounts).sort((a, b) => b[1] - a[1])) { - console.log(` ${bu}: ${cnt} findings`); - } - } - - if (reassigned.length > goneFindings.length * 0.5) { - console.log('\n VERDICT: BU REASSIGNMENT CONFIRMED.'); - } else if (notFound.length > goneFindings.length * 0.5) { - console.log('\n VERDICT: Findings removed from platform entirely (decommission or data purge).'); - } else { - console.log('\n VERDICT: Mixed causes — review individual categories above.'); - } - - db.close(); -} - -main().catch(err => { - console.error('Fatal error:', err); - process.exit(1); -}); diff --git a/backend/scripts/drift-check.js b/backend/scripts/drift-check.js deleted file mode 100644 index 9302db8..0000000 --- a/backend/scripts/drift-check.js +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env node -// drift-check.js — One-time diagnostic to confirm host-level VRR score drift -// -// Queries Ivanti WITHOUT the severity filter for the same BUs, then cross- -// references the results against our archived finding IDs to see if they -// still exist at lower severity scores. -// -// Usage: node backend/scripts/drift-check.js -// -// Output: prints a comparison table and summary. Does NOT modify cve_database.db -// permanently — uses a temporary in-memory table for the comparison. - -require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); - -const path = require('path'); -const sqlite3 = require('sqlite3').verbose(); -const { ivantiPost } = require('../helpers/ivantiApi'); - -const DB_PATH = path.join(__dirname, '..', 'cve_database.db'); - -// Same BU filter, NO severity filter, NO state filter — get everything -const ALL_FINDINGS_FILTERS = [ - { - field: 'assetCustomAttributes.1550_host_1.value', - exclusive: false, - operator: 'IN', - orWithPrevious: false, - implicitFilters: [], - value: 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM', - caseSensitive: false - } -]; - -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 fetchAllFindings(apiKey, clientId, skipTls, state) { - const urlPath = `/client/${encodeURIComponent(clientId)}/hostFinding/search`; - const filters = [ - ...ALL_FINDINGS_FILTERS, - { - field: 'generic_state', - exclusive: false, - operator: 'EXACT', - orWithPrevious: false, - implicitFilters: [], - value: state, - caseSensitive: false - } - ]; - - let allFindings = []; - let page = 0; - let totalPages = 1; - - do { - const body = { - filters, - projection: 'internal', - sort: [{ field: 'severity', direction: 'ASC' }], - page, - size: 100 - }; - - const result = await ivantiPost(urlPath, body, apiKey, skipTls); - if (result.status !== 200) { - console.error(` API returned status ${result.status} on page ${page}`); - break; - } - - const data = JSON.parse(result.body); - totalPages = data.page?.totalPages || 1; - const findings = data._embedded?.hostFindings || []; - - for (const f of findings) { - allFindings.push({ - id: String(f.id), - severity: typeof f.severity === 'number' ? f.severity : parseFloat(f.severity) || 0, - title: f.title || '', - hostName: f.host?.hostName || '', - state - }); - } - - console.error(` ${state} page ${page + 1}/${totalPages} — ${allFindings.length} findings so far`); - page++; - } while (page < totalPages); - - return allFindings; -} - -async function main() { - const apiKey = process.env.IVANTI_API_KEY; - const clientId = process.env.IVANTI_CLIENT_ID || '1550'; - const skipTls = process.env.IVANTI_SKIP_TLS === 'true'; - - if (!apiKey) { - console.error('IVANTI_API_KEY not set in backend/.env'); - process.exit(1); - } - - console.error('=== Drift Check: Querying Ivanti WITHOUT severity filter ===\n'); - - // Fetch all Open findings (no severity filter) - console.error('Fetching ALL Open findings (no severity filter)...'); - const openFindings = await fetchAllFindings(apiKey, clientId, skipTls, 'Open'); - console.error(` Total Open (all severities): ${openFindings.length}\n`); - - // Fetch all Closed findings (no severity filter) - console.error('Fetching ALL Closed findings (no severity filter)...'); - const closedFindings = await fetchAllFindings(apiKey, clientId, skipTls, 'Closed'); - console.error(` Total Closed (all severities): ${closedFindings.length}\n`); - - const allFindings = [...openFindings, ...closedFindings]; - const findingMap = new Map(allFindings.map(f => [f.id, f])); - - console.error(`Total findings across both states: ${allFindings.length}\n`); - - // Open the database and get archived finding IDs - const db = new sqlite3.Database(DB_PATH); - - const archived = await dbAll(db, - `SELECT finding_id, last_severity, finding_title, host_name, current_state - FROM ivanti_finding_archives - WHERE current_state IN ('ARCHIVED', 'CLOSED') - ORDER BY current_state, last_severity DESC` - ); - - console.log(''); - console.log('='.repeat(120)); - console.log('DRIFT CHECK RESULTS'); - console.log('='.repeat(120)); - console.log(''); - - // Categorize results - const drifted = []; // Found in API at lower severity (below 8.5) - const stillHigh = []; // Found in API, severity still >= 8.5 - const gone = []; // Not found in API at all (any severity) - const stateChanged = []; // Found but in different state - - for (const arch of archived) { - const current = findingMap.get(arch.finding_id); - if (!current) { - gone.push(arch); - } else if (current.severity < 8.5) { - drifted.push({ ...arch, currentSeverity: current.severity, currentState: current.state }); - } else { - stillHigh.push({ ...arch, currentSeverity: current.severity, currentState: current.state }); - } - } - - // Print drifted findings - console.log(`CONFIRMED SCORE DRIFT (now below 8.5): ${drifted.length} findings`); - console.log('-'.repeat(120)); - if (drifted.length > 0) { - console.log( - 'Finding ID'.padEnd(14) + - 'Archive State'.padEnd(15) + - 'Last Severity'.padEnd(15) + - 'Current Severity'.padEnd(18) + - 'Delta'.padEnd(10) + - 'Current State'.padEnd(15) + - 'Title' - ); - console.log('-'.repeat(120)); - for (const f of drifted) { - const delta = (f.currentSeverity - f.last_severity).toFixed(2); - console.log( - f.finding_id.padEnd(14) + - f.current_state.padEnd(15) + - f.last_severity.toFixed(2).padEnd(15) + - f.currentSeverity.toFixed(2).padEnd(18) + - delta.padEnd(10) + - f.currentState.padEnd(15) + - f.finding_title.substring(0, 50) - ); - } - } - - console.log(''); - console.log(`STILL HIGH SEVERITY (>= 8.5, should be in filtered results): ${stillHigh.length} findings`); - console.log('-'.repeat(120)); - if (stillHigh.length > 0) { - console.log( - 'Finding ID'.padEnd(14) + - 'Archive State'.padEnd(15) + - 'Last Severity'.padEnd(15) + - 'Current Severity'.padEnd(18) + - 'Current State'.padEnd(15) + - 'Title' - ); - console.log('-'.repeat(120)); - for (const f of stillHigh) { - console.log( - f.finding_id.padEnd(14) + - f.current_state.padEnd(15) + - f.last_severity.toFixed(2).padEnd(15) + - f.currentSeverity.toFixed(2).padEnd(18) + - f.currentState.padEnd(15) + - f.finding_title.substring(0, 50) - ); - } - } - - console.log(''); - console.log(`COMPLETELY GONE (not in API at any severity): ${gone.length} findings`); - console.log('-'.repeat(120)); - if (gone.length > 0) { - console.log( - 'Finding ID'.padEnd(14) + - 'Archive State'.padEnd(15) + - 'Last Severity'.padEnd(15) + - 'Title' - ); - console.log('-'.repeat(120)); - for (const f of gone) { - console.log( - f.finding_id.padEnd(14) + - f.current_state.padEnd(15) + - f.last_severity.toFixed(2).padEnd(15) + - f.finding_title.substring(0, 50) - ); - } - } - - // Summary - console.log(''); - console.log('='.repeat(120)); - console.log('SUMMARY'); - console.log('='.repeat(120)); - console.log(` Archived/Closed findings checked: ${archived.length}`); - console.log(` Confirmed score drift (< 8.5): ${drifted.length}`); - console.log(` Still high severity (>= 8.5): ${stillHigh.length}`); - console.log(` Completely gone from API: ${gone.length}`); - console.log(''); - - if (drifted.length > 0) { - const avgDelta = drifted.reduce((sum, f) => sum + (f.currentSeverity - f.last_severity), 0) / drifted.length; - const minNew = Math.min(...drifted.map(f => f.currentSeverity)); - const maxNew = Math.max(...drifted.map(f => f.currentSeverity)); - console.log(` Score drift range: ${minNew.toFixed(2)} – ${maxNew.toFixed(2)} (avg delta: ${avgDelta.toFixed(2)})`); - } - - if (drifted.length > archived.length * 0.5) { - console.log('\n VERDICT: Host-level VRR score drift CONFIRMED.'); - console.log(' The majority of disappeared findings still exist in Ivanti but at lower severity scores.'); - } else if (drifted.length > 0) { - console.log('\n VERDICT: Partial score drift detected. Some findings drifted, others may have been removed.'); - } else if (gone.length > archived.length * 0.5) { - console.log('\n VERDICT: Score drift NOT confirmed. Most findings are completely gone from the API.'); - console.log(' This suggests BU reassignment, host decommission, or a platform-side data issue.'); - } - - db.close(); -} - -main().catch(err => { - console.error('Fatal error:', err); - process.exit(1); -}); diff --git a/backend/scripts/export-reassigned-findings.js b/backend/scripts/export-reassigned-findings.js deleted file mode 100644 index 9c53667..0000000 --- a/backend/scripts/export-reassigned-findings.js +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env node -// export-reassigned-findings.js — Generate an xlsx with findings reassigned to SDIT-CSD-ITLS-PIES -// -// Pulls data from the archive database and the BU reassignment check results. -// Outputs to docs/reassigned-findings-2026-04-24.xlsx -// -// Usage: node backend/scripts/export-reassigned-findings.js - -require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }); - -const path = require('path'); -const sqlite3 = require('sqlite3').verbose(); -const XLSX = require('xlsx'); -const { ivantiPost } = require('../helpers/ivantiApi'); - -const DB_PATH = path.join(__dirname, '..', 'cve_database.db'); -const OUTPUT_PATH = path.join(__dirname, '..', '..', 'docs', 'reassigned-findings-2026-04-24.xlsx'); - -function dbAll(db, sql, params = []) { - return new Promise((resolve, reject) => { - db.all(sql, params, (err, rows) => { - if (err) reject(err); - else resolve(rows || []); - }); - }); -} - -async function queryByFindingIds(findingIds, apiKey, clientId, skipTls) { - const urlPath = `/client/${encodeURIComponent(clientId)}/hostFinding/search`; - const results = new Map(); - const chunkSize = 50; - - for (let i = 0; i < findingIds.length; i += chunkSize) { - const chunk = findingIds.slice(i, i + chunkSize); - const idList = chunk.join(','); - const filters = [{ - field: 'id', exclusive: false, operator: 'IN', - orWithPrevious: false, implicitFilters: [], - value: idList, caseSensitive: false - }]; - - let page = 0; - let totalPages = 1; - do { - try { - const body = { filters, projection: 'internal', sort: [{ field: 'severity', direction: 'ASC' }], page, size: 100 }; - const result = await ivantiPost(urlPath, body, apiKey, skipTls); - if (result.status !== 200) break; - const data = JSON.parse(result.body); - totalPages = data.page?.totalPages || 1; - const findings = data._embedded?.hostFindings || []; - for (const f of findings) { - const bu = f.assetCustomAttributes?.['1550_host_1']?.[0] || 'UNKNOWN'; - const wfDist = f.workflowDistribution || {}; - const fpBuckets = [ - ...(wfDist.approvedWorkflows || []), ...(wfDist.actionableWorkflows || []), - ...(wfDist.requestedWorkflows || []), ...(wfDist.reworkedWorkflows || []), - ...(wfDist.rejectedWorkflows || []), ...(wfDist.expiredWorkflows || []), - ].filter(w => (w.generatedId || '').startsWith('FP#')); - const fp = fpBuckets[0] || null; - results.set(String(f.id), { - bu, - severity: typeof f.severity === 'number' ? f.severity : parseFloat(f.severity) || 0, - state: f.status || '', - fpId: fp ? fp.generatedId : '', - fpState: fp ? fp.state : '', - hostName: f.host?.hostName || '', - ipAddress: f.host?.ipAddress || '', - title: f.title || '', - }); - } - page++; - } catch (err) { - console.error(` Error on batch at ${i}:`, err.message); - break; - } - } while (page < totalPages); - } - return results; -} - -async function main() { - const apiKey = process.env.IVANTI_API_KEY; - const clientId = process.env.IVANTI_CLIENT_ID || '1550'; - const skipTls = process.env.IVANTI_SKIP_TLS === 'true'; - - const db = new sqlite3.Database(DB_PATH); - - // Get all archived/closed findings from the archive - const archived = await dbAll(db, - `SELECT finding_id, last_severity, finding_title, host_name, ip_address, current_state, - DATE(first_archived_at) as archived_date, DATE(last_transition_at) as last_transition_date - FROM ivanti_finding_archives - WHERE current_state IN ('ARCHIVED', 'CLOSED') - ORDER BY current_state, last_severity DESC` - ); - - const ids = archived.map(a => a.finding_id); - console.log(`Querying Ivanti for ${ids.length} findings...`); - const currentData = await queryByFindingIds(ids, apiKey, clientId, skipTls); - - // Build rows for each sheet - const reassignedRows = []; - const goneRows = []; - const sameBuRows = []; - - for (const arch of archived) { - const current = currentData.get(arch.finding_id); - - if (!current) { - goneRows.push({ - 'Finding ID': arch.finding_id, - 'Title': arch.finding_title, - 'Last Severity': arch.last_severity, - 'Host': arch.host_name, - 'IP Address': arch.ip_address, - 'Archive State': arch.current_state, - 'Archived Date': arch.archived_date, - 'Status': 'Gone from platform', - }); - } else if (current.bu !== 'NTS-AEO-ACCESS-ENG' && current.bu !== 'NTS-AEO-STEAM') { - reassignedRows.push({ - 'Finding ID': arch.finding_id, - 'Title': current.title || arch.finding_title, - 'Last Severity (STEAM)': arch.last_severity, - 'Current Severity': current.severity, - 'Host': current.hostName || arch.host_name, - 'IP Address': current.ipAddress || arch.ip_address, - 'Previous BU': 'NTS-AEO-STEAM / ACCESS-ENG', - 'Current BU': current.bu, - 'Current State': current.state, - 'FP Workflow': current.fpId ? `${current.fpId} (${current.fpState})` : '', - 'Archive State': arch.current_state, - 'Archived Date': arch.archived_date, - }); - } else { - sameBuRows.push({ - 'Finding ID': arch.finding_id, - 'Title': current.title || arch.finding_title, - 'Severity': current.severity, - 'Host': current.hostName || arch.host_name, - 'IP Address': current.ipAddress || arch.ip_address, - 'BU': current.bu, - 'Current State': current.state, - 'FP Workflow': current.fpId ? `${current.fpId} (${current.fpState})` : '', - 'Archive State': arch.current_state, - }); - } - } - - // Create workbook - const wb = XLSX.utils.book_new(); - - // Sheet 1: Reassigned findings - const ws1 = XLSX.utils.json_to_sheet(reassignedRows); - // Set column widths - ws1['!cols'] = [ - { wch: 14 }, { wch: 55 }, { wch: 18 }, { wch: 16 }, - { wch: 30 }, { wch: 16 }, { wch: 28 }, { wch: 24 }, - { wch: 14 }, { wch: 24 }, { wch: 14 }, { wch: 14 }, - ]; - XLSX.utils.book_append_sheet(wb, ws1, 'Reassigned to SDIT-PIES'); - - // Sheet 2: Gone from platform - if (goneRows.length > 0) { - const ws2 = XLSX.utils.json_to_sheet(goneRows); - ws2['!cols'] = [ - { wch: 14 }, { wch: 55 }, { wch: 14 }, { wch: 30 }, - { wch: 16 }, { wch: 14 }, { wch: 14 }, { wch: 20 }, - ]; - XLSX.utils.book_append_sheet(wb, ws2, 'Gone from Platform'); - } - - // Sheet 3: Still same BU - if (sameBuRows.length > 0) { - const ws3 = XLSX.utils.json_to_sheet(sameBuRows); - ws3['!cols'] = [ - { wch: 14 }, { wch: 55 }, { wch: 10 }, { wch: 30 }, - { wch: 16 }, { wch: 24 }, { wch: 14 }, { wch: 24 }, { wch: 14 }, - ]; - XLSX.utils.book_append_sheet(wb, ws3, 'Still Same BU'); - } - - // Write file - XLSX.writeFile(wb, OUTPUT_PATH); - console.log(`\nExported to: ${OUTPUT_PATH}`); - console.log(` Reassigned: ${reassignedRows.length}`); - console.log(` Gone: ${goneRows.length}`); - console.log(` Same BU: ${sameBuRows.length}`); - - db.close(); -} - -main().catch(err => { - console.error('Fatal error:', err); - process.exit(1); -}); diff --git a/docs/reassigned-findings-2026-04-24.xlsx b/docs/reassigned-findings-2026-04-24.xlsx deleted file mode 100644 index 14889c4..0000000 Binary files a/docs/reassigned-findings-2026-04-24.xlsx and /dev/null differ