From 69809955a9b2679ceaf717e4a3b7b71b8e04767f Mon Sep 17 00:00:00 2001 From: root Date: Fri, 24 Apr 2026 20:36:46 +0000 Subject: [PATCH] Remove diagnostic scripts and xlsx export from tracking, add to gitignore --- .gitignore | 8 + backend/scripts/bu-reassignment-check.js | 270 ----------------- backend/scripts/drift-check.js | 275 ------------------ backend/scripts/export-reassigned-findings.js | 197 ------------- docs/reassigned-findings-2026-04-24.xlsx | Bin 89038 -> 0 bytes 5 files changed, 8 insertions(+), 742 deletions(-) delete mode 100644 backend/scripts/bu-reassignment-check.js delete mode 100644 backend/scripts/drift-check.js delete mode 100644 backend/scripts/export-reassigned-findings.js delete mode 100644 docs/reassigned-findings-2026-04-24.xlsx 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 14889c464b720417e85be11a1f5820c600589e7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89038 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 Q{`Vb%{(t(COz`&pA5K1*rT_o{