198 lines
7.9 KiB
JavaScript
198 lines
7.9 KiB
JavaScript
|
|
#!/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);
|
||
|
|
});
|