Fix Ivanti panel bugs: Invalid Date, wrong workflow count, crash on archive click, BU scope filtering
This commit is contained in:
@@ -33,9 +33,25 @@ function createIvantiArchiveRouter() {
|
||||
// All routes require authentication
|
||||
router.use(requireAuth());
|
||||
|
||||
// GET / — List archive records with optional state filtering
|
||||
/**
|
||||
* GET /
|
||||
* List archive records with optional state and teams filtering.
|
||||
*
|
||||
* @query {string} [state] - Filter by lifecycle state. Valid values: ACTIVE, ARCHIVED, RETURNED, CLOSED.
|
||||
* When state=ACTIVE, returns live open findings from ivanti_findings instead of archives.
|
||||
* When state=CLOSED, includes both CLOSED and CLOSED_GONE records.
|
||||
* @query {string} [teams] - Comma-separated BU team names (e.g. 'STEAM,ACCESS-ENG').
|
||||
* Filters results to findings whose bu_ownership contains one of the specified teams.
|
||||
*
|
||||
* @response {object} 200
|
||||
* { archives: Array<{ id, finding_id, finding_title, host_name, ip_address, current_state, last_severity, first_archived_at, last_transition_at, created_at, related_active: object|null }>, total: number }
|
||||
* @response {object} 400 - Invalid state parameter
|
||||
* { error: string }
|
||||
* @response {object} 500 - Database error
|
||||
* { error: string }
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
const { state } = req.query;
|
||||
const { state, teams } = req.query;
|
||||
|
||||
if (state && !VALID_STATES.includes(state)) {
|
||||
return res.status(400).json({
|
||||
@@ -43,17 +59,64 @@ function createIvantiArchiveRouter() {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let query = 'SELECT * FROM ivanti_finding_archives';
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
// Parse teams filter into ILIKE patterns
|
||||
const teamPatterns = teams
|
||||
? teams.split(',').map(t => `%${t.trim()}%`).filter(p => p !== '%%')
|
||||
: [];
|
||||
|
||||
if (state) {
|
||||
query += ` WHERE current_state = $${paramIndex++}`;
|
||||
params.push(state);
|
||||
try {
|
||||
// ACTIVE state comes from ivanti_findings (live open findings), not archives
|
||||
if (state === 'ACTIVE') {
|
||||
let activeQuery = `SELECT id, id AS finding_id, title AS finding_title, host_name, ip_address,
|
||||
'ACTIVE' AS current_state, severity AS last_severity,
|
||||
synced_at AS first_archived_at, synced_at AS last_transition_at, synced_at AS created_at
|
||||
FROM ivanti_findings WHERE state = 'open'`;
|
||||
const activeParams = [];
|
||||
let activeIdx = 1;
|
||||
if (teamPatterns.length > 0) {
|
||||
activeQuery += ` AND bu_ownership ILIKE ANY($${activeIdx++}::text[])`;
|
||||
activeParams.push(teamPatterns);
|
||||
}
|
||||
activeQuery += ` ORDER BY severity DESC NULLS LAST LIMIT 200`;
|
||||
const { rows: activeRows } = await pool.query(activeQuery, activeParams);
|
||||
const archives = activeRows.map(r => ({ ...r, related_active: null }));
|
||||
return res.json({ archives, total: archives.length });
|
||||
}
|
||||
|
||||
query += ' ORDER BY last_transition_at DESC';
|
||||
// For non-ACTIVE states, query archives with optional BU join
|
||||
let query, params = [], paramIndex = 1;
|
||||
|
||||
if (teamPatterns.length > 0) {
|
||||
// JOIN with ivanti_findings to filter by bu_ownership
|
||||
query = `SELECT a.* FROM ivanti_finding_archives a
|
||||
INNER JOIN ivanti_findings f ON a.finding_id = f.id
|
||||
WHERE f.bu_ownership ILIKE ANY($${paramIndex++}::text[])`;
|
||||
params.push(teamPatterns);
|
||||
if (state) {
|
||||
if (state === 'CLOSED') {
|
||||
query += ` AND a.current_state IN ($${paramIndex++}, $${paramIndex++})`;
|
||||
params.push('CLOSED', 'CLOSED_GONE');
|
||||
} else {
|
||||
query += ` AND a.current_state = $${paramIndex++}`;
|
||||
params.push(state);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query = 'SELECT * FROM ivanti_finding_archives';
|
||||
if (state) {
|
||||
if (state === 'CLOSED') {
|
||||
query += ` WHERE current_state IN ($${paramIndex++}, $${paramIndex++})`;
|
||||
params.push('CLOSED', 'CLOSED_GONE');
|
||||
} else {
|
||||
query += ` WHERE current_state = $${paramIndex++}`;
|
||||
params.push(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query += teamPatterns.length > 0
|
||||
? ' ORDER BY a.last_transition_at DESC'
|
||||
: ' ORDER BY last_transition_at DESC';
|
||||
|
||||
const { rows: archives } = await pool.query(query, params);
|
||||
|
||||
@@ -82,27 +145,60 @@ function createIvantiArchiveRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /stats — Summary counts by lifecycle state
|
||||
/**
|
||||
* GET /stats
|
||||
* Summary counts of archive records grouped by lifecycle state.
|
||||
*
|
||||
* @query {string} [teams] - Comma-separated BU team names (e.g. 'STEAM,ACCESS-ENG').
|
||||
* Filters counts to findings whose bu_ownership contains one of the specified teams.
|
||||
*
|
||||
* @response {object} 200
|
||||
* { ACTIVE: number, ARCHIVED: number, RETURNED: number, CLOSED: number, total: number }
|
||||
* @response {object} 500 - Database error
|
||||
* { error: string }
|
||||
*/
|
||||
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 { teams } = req.query;
|
||||
const teamPatterns = teams
|
||||
? teams.split(',').map(t => `%${t.trim()}%`).filter(p => p !== '%%')
|
||||
: [];
|
||||
|
||||
let archiveQuery, archiveParams = [];
|
||||
if (teamPatterns.length > 0) {
|
||||
archiveQuery = `SELECT a.current_state, COUNT(*) as count
|
||||
FROM ivanti_finding_archives a
|
||||
INNER JOIN ivanti_findings f ON a.finding_id = f.id
|
||||
WHERE f.bu_ownership ILIKE ANY($1::text[])
|
||||
GROUP BY a.current_state`;
|
||||
archiveParams = [teamPatterns];
|
||||
} else {
|
||||
archiveQuery = `SELECT current_state, COUNT(*) as count
|
||||
FROM ivanti_finding_archives
|
||||
GROUP BY current_state`;
|
||||
}
|
||||
|
||||
const { rows } = await pool.query(archiveQuery, archiveParams);
|
||||
|
||||
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);
|
||||
stats[row.current_state] += parseInt(row.count);
|
||||
} else if (row.current_state === 'CLOSED_GONE') {
|
||||
stats.CLOSED += parseInt(row.count);
|
||||
}
|
||||
}
|
||||
|
||||
// ACTIVE = total live findings count
|
||||
const countResult = await pool.query(
|
||||
`SELECT COUNT(*) as total FROM ivanti_findings WHERE state = 'open'`
|
||||
);
|
||||
// ACTIVE = total live findings count (scoped by teams if provided)
|
||||
let activeQuery, activeParams = [];
|
||||
if (teamPatterns.length > 0) {
|
||||
activeQuery = `SELECT COUNT(*) as total FROM ivanti_findings WHERE state = 'open' AND bu_ownership ILIKE ANY($1::text[])`;
|
||||
activeParams = [teamPatterns];
|
||||
} else {
|
||||
activeQuery = `SELECT COUNT(*) as total FROM ivanti_findings WHERE state = 'open'`;
|
||||
}
|
||||
const countResult = await pool.query(activeQuery, activeParams);
|
||||
stats.ACTIVE = parseInt(countResult.rows[0].total) || 0;
|
||||
|
||||
const total = stats.ACTIVE + stats.ARCHIVED + stats.RETURNED + stats.CLOSED;
|
||||
@@ -114,7 +210,17 @@ function createIvantiArchiveRouter() {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /:findingId/history — Transition history for a specific archived finding
|
||||
/**
|
||||
* GET /:findingId/history
|
||||
* Transition history for a specific archived finding.
|
||||
*
|
||||
* @param {string} findingId - The finding ID to look up in the archives.
|
||||
*
|
||||
* @response {object} 200
|
||||
* { finding_id: string, transitions: Array<{ id, archive_id, from_state, to_state, transitioned_at, reason }> }
|
||||
* @response {object} 500 - Database error
|
||||
* { error: string }
|
||||
*/
|
||||
router.get('/:findingId/history', async (req, res) => {
|
||||
const { findingId } = req.params;
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ async function readState() {
|
||||
try { workflows = JSON.parse(row.workflows_json || '[]'); } catch (_) { /* leave empty */ }
|
||||
|
||||
return {
|
||||
total: row.total || 0,
|
||||
total: workflows.length,
|
||||
workflows,
|
||||
synced_at: row.synced_at,
|
||||
sync_status: row.sync_status,
|
||||
|
||||
Reference in New Issue
Block a user