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