Add SVP org hierarchy discovery to BU Lookup panel

Backend:
- GET /api/ivanti/findings/org-hierarchy — returns all SVP names and
  all BU names from Ivanti suggest API (Admin only)
- GET /api/ivanti/findings/bus-by-svp?svp=<name> — returns BUs under
  a specific SVP with finding counts (Admin only)

Frontend (Admin > BU Lookup tab):
- 'Load Org Hierarchy' button fetches all SVPs and BUs
- SVP dropdown (sorted by finding count) to filter BUs by org leader
- Results table shows BU name, finding count, and configured status
  (green badge for BUs already in the dashboard, grey for unconfigured)
- Enables admin to discover which BUs roll up under an SVP for correct
  team assignment when onboarding new users
This commit is contained in:
Jordan Ramos
2026-06-26 09:33:45 -06:00
parent 58996cf4cf
commit 93de7bbca7
2 changed files with 231 additions and 0 deletions

View File

@@ -1766,6 +1766,84 @@ function createIvantiFindingsRouter(db, requireAuth) {
}
});
/**
* GET /api/ivanti/findings/org-hierarchy
*
* Return the full SVP list and BU list from the Ivanti suggest API.
* Used by admin tooling to display the organizational hierarchy for
* BU filter configuration and team assignment verification.
* Requires Admin group.
*
* @returns {Object} 200 - { svps: string[], bus: string[] }
* @returns {Object} 503 - { error: string } when Ivanti API key is not configured
* @returns {Object} 500 - { error: string } on API failure
*/
router.get('/org-hierarchy', requireGroup('Admin'), async (req, res) => {
try {
const clientId = process.env.IVANTI_CLIENT_ID || '1550';
const apiKey = process.env.IVANTI_API_KEY;
const skipTls = process.env.IVANTI_SKIP_TLS === 'true';
if (!apiKey) return res.status(503).json({ error: 'Ivanti API key not configured' });
// Fetch SVP values
const svpResult = await ivantiPost(`/client/${clientId}/hostFinding/suggest`, {
filter: { field: 'assetCustomAttributes.1550_host_10.value', operator: 'WILDCARD', value: '*', exclusive: false }
}, apiKey, skipTls);
// Fetch all BU values
const buResult = await ivantiPost(`/client/${clientId}/hostFinding/suggest`, {
filter: { field: 'assetCustomAttributes.1550_host_1.value', operator: 'WILDCARD', value: '*', exclusive: false }
}, apiKey, skipTls);
const svps = svpResult.status === 200 ? JSON.parse(svpResult.body) : [];
const bus = buResult.status === 200 ? JSON.parse(buResult.body) : [];
res.json({ svps, bus });
} catch (err) {
console.error('[Ivanti] Org hierarchy error:', err.message);
res.status(500).json({ error: 'Failed to fetch org hierarchy' });
}
});
/**
* GET /api/ivanti/findings/bus-by-svp
*
* Return BU names that belong to a specific SVP, queried from the Ivanti
* suggest API with the SVP as a filter. Used for cascading dropdowns in
* admin configuration UIs.
* Requires Admin group.
*
* @query {string} svp - The SVP name to filter BUs by (required)
*
* @returns {Object} 200 - { svp: string, bus: string[] }
* @returns {Object} 400 - { error: string } when svp query parameter is missing
* @returns {Object} 503 - { error: string } when Ivanti API key is not configured
* @returns {Object} 500 - { error: string } on API failure
*/
router.get('/bus-by-svp', requireGroup('Admin'), async (req, res) => {
const svp = (req.query.svp || '').trim();
if (!svp) return res.status(400).json({ error: 'svp query parameter is required' });
try {
const clientId = process.env.IVANTI_CLIENT_ID || '1550';
const apiKey = process.env.IVANTI_API_KEY;
const skipTls = process.env.IVANTI_SKIP_TLS === 'true';
if (!apiKey) return res.status(503).json({ error: 'Ivanti API key not configured' });
// Get BUs filtered by the selected SVP
const result = await ivantiPost(`/client/${clientId}/hostFinding/suggest`, {
filter: { field: 'assetCustomAttributes.1550_host_1.value', operator: 'WILDCARD', value: '*', exclusive: false },
filters: [{ field: 'assetCustomAttributes.1550_host_10.value', operator: 'EXACT', value: svp, exclusive: false }]
}, apiKey, skipTls);
const bus = result.status === 200 ? JSON.parse(result.body) : [];
res.json({ svp, bus });
} catch (err) {
console.error('[Ivanti] BUs by SVP error:', err.message);
res.status(500).json({ error: 'Failed to fetch BUs for SVP' });
}
});
return router;
}