Remove diagnostic scripts and xlsx export from tracking, add to gitignore
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -61,3 +61,11 @@ backend/setup.js-backup
|
|||||||
|
|
||||||
# Kiro implementation summary (internal only)
|
# Kiro implementation summary (internal only)
|
||||||
docs/kiro-implementation-summary.md
|
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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user