// Ivanti Archive Routes — list, stats, and transition history for archived findings const express = require('express'); const pool = require('../db'); const { requireAuth } = require('../middleware/auth'); const VALID_STATES = ['ACTIVE', 'ARCHIVED', 'RETURNED', 'CLOSED']; /** * Find the most severe active finding related to an archived finding. */ 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() { const router = express.Router(); // All routes require authentication router.use(requireAuth()); // GET / — List archive records with optional state filtering router.get('/', async (req, res) => { const { state } = req.query; if (state && !VALID_STATES.includes(state)) { return res.status(400).json({ error: 'Invalid state parameter. Valid values: ACTIVE, ARCHIVED, RETURNED, CLOSED' }); } try { let query = 'SELECT * FROM ivanti_finding_archives'; const params = []; let paramIndex = 1; if (state) { query += ` WHERE current_state = $${paramIndex++}`; params.push(state); } query += ' ORDER BY last_transition_at DESC'; const { rows: archives } = await pool.query(query, params); // Fetch active findings for related-finding enrichment // In the new schema, active findings are in ivanti_findings table let activeFindings = []; try { const { rows: findingsRows } = await pool.query( `SELECT id, title, host_name AS "hostName", severity FROM ivanti_findings WHERE state = 'open'` ); activeFindings = findingsRows; } catch (cacheErr) { console.warn('Failed to load findings for related-active matching:', cacheErr); } // 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' }); } }); // GET /stats — Summary counts by lifecycle state router.get('/stats', async (req, res) => { try { const { rows } = await pool.query( `SELECT current_state, COUNT(*) as count FROM ivanti_finding_archives GROUP BY current_state` ); const stats = { ACTIVE: 0, ARCHIVED: 0, RETURNED: 0, CLOSED: 0 }; for (const row of rows) { if (stats.hasOwnProperty(row.current_state)) { stats[row.current_state] = parseInt(row.count); } } // ACTIVE = total live findings count const countResult = await pool.query( `SELECT COUNT(*) as total FROM ivanti_findings WHERE state = 'open'` ); stats.ACTIVE = parseInt(countResult.rows[0].total) || 0; const total = stats.ACTIVE + stats.ARCHIVED + stats.RETURNED + stats.CLOSED; res.json({ ...stats, total }); } catch (err) { console.error('Archive stats error:', err); res.status(500).json({ error: 'Failed to fetch archive stats' }); } }); // GET /:findingId/history — Transition history for a specific archived finding router.get('/:findingId/history', async (req, res) => { const { findingId } = req.params; try { const { rows: archiveRows } = await pool.query( 'SELECT id FROM ivanti_finding_archives WHERE finding_id = $1', [findingId] ); const archive = archiveRows[0]; if (!archive) { return res.json({ finding_id: findingId, transitions: [] }); } const { rows: transitions } = await pool.query( `SELECT * FROM ivanti_archive_transitions WHERE archive_id = $1 ORDER BY transitioned_at DESC`, [archive.id] ); res.json({ finding_id: findingId, transitions }); } catch (err) { console.error('Archive history error:', err); res.status(500).json({ error: 'Failed to fetch transition history' }); } }); return router; } module.exports = createIvantiArchiveRouter;