From cf43e85c38e11d11352cfda6f3069be3784ff7a0 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Wed, 6 May 2026 15:19:34 -0600 Subject: [PATCH] fix: scope FP workflow counts donut by BU - Rewrite /fp-workflow-counts endpoint to query ivanti_findings table directly with optional teams ILIKE filter (replaces pre-computed JSON blob) - Frontend passes getActiveTeamsParam() to FP counts fetch - FP counts refresh on scope toggle change alongside open/closed counts - Both FP Finding Status and FP Workflow Status donuts now respect BU scope --- backend/routes/ivantiFindings.js | 50 ++++++++++++++++--- .../src/components/pages/ReportingPage.js | 8 ++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/backend/routes/ivantiFindings.js b/backend/routes/ivantiFindings.js index 6ffe5ea..261d48c 100644 --- a/backend/routes/ivantiFindings.js +++ b/backend/routes/ivantiFindings.js @@ -1222,18 +1222,56 @@ function createIvantiFindingsRouter(db, requireAuth) { * * Return FP finding counts and unique workflow ID counts (open + closed), * broken down by workflow status. + * Accepts optional `teams` query parameter to scope to specific BUs. * + * @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG') * @returns {Object} 200 - { findingCounts: Object, findingTotal: number, idCounts: Object, idTotal: number } * @returns {Object} 500 - { error: string } on database error */ router.get('/fp-workflow-counts', async (req, res) => { try { - const { rows } = await pool.query('SELECT fp_workflow_counts_json, fp_id_counts_json FROM ivanti_counts_cache WHERE id=1'); - const row = rows[0]; - let findingCounts = {}; - let idCounts = {}; - try { findingCounts = JSON.parse(row?.fp_workflow_counts_json || '{}'); } catch (_) {} - try { idCounts = JSON.parse(row?.fp_id_counts_json || '{}'); } catch (_) {} + const teamsParam = req.query.teams; + let whereExtra = ''; + const params = []; + let paramIndex = 1; + + if (teamsParam) { + const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean); + if (teams.length > 0) { + const patterns = teams.map(t => `%${t}%`); + whereExtra = ` AND bu_ownership ILIKE ANY($${paramIndex++}::text[])`; + params.push(patterns); + } + } + + // Finding counts: number of findings per workflow state + const findingResult = await pool.query( + `SELECT workflow_state, COUNT(*) as count + FROM ivanti_findings + WHERE workflow_id IS NOT NULL ${whereExtra} + GROUP BY workflow_state`, + params + ); + const findingCounts = {}; + findingResult.rows.forEach(r => { + const state = r.workflow_state || 'Unknown'; + findingCounts[state] = parseInt(r.count); + }); + + // ID counts: number of unique workflow IDs per state + const idResult = await pool.query( + `SELECT workflow_state, COUNT(DISTINCT workflow_id) as count + FROM ivanti_findings + WHERE workflow_id IS NOT NULL ${whereExtra} + GROUP BY workflow_state`, + params + ); + const idCounts = {}; + idResult.rows.forEach(r => { + const state = r.workflow_state || 'Unknown'; + idCounts[state] = parseInt(r.count); + }); + res.json({ findingCounts, findingTotal: Object.values(findingCounts).reduce((a, b) => a + b, 0), diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js index 86570cf..ce73f04 100644 --- a/frontend/src/components/pages/ReportingPage.js +++ b/frontend/src/components/pages/ReportingPage.js @@ -5069,7 +5069,11 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { const fetchFPWorkflowCounts = async () => { try { - const res = await fetch(`${API_BASE}/ivanti/findings/fp-workflow-counts`, { credentials: 'include' }); + const teamsParam = getActiveTeamsParam(); + const url = teamsParam + ? `${API_BASE}/ivanti/findings/fp-workflow-counts?teams=${encodeURIComponent(teamsParam)}` + : `${API_BASE}/ivanti/findings/fp-workflow-counts`; + const res = await fetch(url, { credentials: 'include' }); const data = await res.json(); if (res.ok) setFPCounts({ findingCounts: data.findingCounts || {}, @@ -5197,6 +5201,8 @@ export default function VulnerabilityTriagePage({ filterDate, filterEXC }) { .then(r => r.ok ? r.json() : null) .then(data => { if (data) setStatusCounts({ open: data.open ?? 0, closed: data.closed ?? 0 }); }) .catch(() => {}); + // Also refresh FP workflow counts for the new scope + fetchFPWorkflowCounts(); }, [adminScope]); // eslint-disable-line // Set/clear a single column filter