feat: improve archive finding clarity with finding IDs, historical severity labels, and related active finding indicators
This commit is contained in:
@@ -3,6 +3,37 @@ const express = require('express');
|
||||
|
||||
const VALID_STATES = ['ACTIVE', 'ARCHIVED', 'RETURNED', 'CLOSED'];
|
||||
|
||||
/**
|
||||
* Find the most severe active finding related to an archived finding.
|
||||
*
|
||||
* A match requires:
|
||||
* - Exact hostname match (case-sensitive)
|
||||
* - The archive title is a case-insensitive substring of the active title, or vice versa
|
||||
* - The active finding ID differs from the archive's finding_id
|
||||
*
|
||||
* @param {Object} archive - Archive record from ivanti_finding_archives
|
||||
* @param {Array} activeFindings - Parsed entries from ivanti_findings_cache
|
||||
* @returns {{ id: string, title: string, severity: number } | null}
|
||||
*/
|
||||
function findRelatedActive(archive, activeFindings) {
|
||||
const archiveTitle = (archive.finding_title || '').toLowerCase();
|
||||
|
||||
const matches = activeFindings.filter(f => {
|
||||
if (f.hostName !== archive.host_name) return false;
|
||||
if (f.id === archive.finding_id) return false;
|
||||
|
||||
const activeTitle = (f.title || '').toLowerCase();
|
||||
if (!archiveTitle.includes(activeTitle) && !activeTitle.includes(archiveTitle)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matches.length === 0) return null;
|
||||
|
||||
const best = matches.reduce((a, b) => (b.severity > a.severity ? b : a));
|
||||
return { id: best.id, title: best.title, severity: best.severity };
|
||||
}
|
||||
|
||||
function createIvantiArchiveRouter(db, requireAuth) {
|
||||
const router = express.Router();
|
||||
|
||||
@@ -45,7 +76,37 @@ function createIvantiArchiveRouter(db, requireAuth) {
|
||||
});
|
||||
});
|
||||
|
||||
res.json({ archives, total: archives.length });
|
||||
// Fetch and parse active findings cache for related-finding enrichment
|
||||
let activeFindings = [];
|
||||
try {
|
||||
const cacheRow = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT findings_json FROM ivanti_findings_cache WHERE id = 1',
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (cacheRow && cacheRow.findings_json) {
|
||||
activeFindings = JSON.parse(cacheRow.findings_json);
|
||||
}
|
||||
} catch (cacheErr) {
|
||||
console.warn('Failed to load findings cache for related-active matching:', cacheErr);
|
||||
}
|
||||
|
||||
if (!Array.isArray(activeFindings)) {
|
||||
activeFindings = [];
|
||||
}
|
||||
|
||||
// Enrich each archive record with related active finding info
|
||||
const enrichedArchives = archives.map(archive => ({
|
||||
...archive,
|
||||
related_active: findRelatedActive(archive, activeFindings)
|
||||
}));
|
||||
|
||||
res.json({ archives: enrichedArchives, total: enrichedArchives.length });
|
||||
} catch (err) {
|
||||
console.error('Archive list error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch archive records' });
|
||||
|
||||
Reference in New Issue
Block a user