From 58996cf4cf287345f622d61ab85fc9b68545d5d8 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Fri, 26 Jun 2026 09:20:53 -0600 Subject: [PATCH] Add BU Lookup tool to Admin panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - GET /api/ivanti/findings/bu-lookup?q= — queries the live Ivanti API to discover which BU a host is assigned to. Returns deduplicated results with hostName, ipAddress, BU, and hostId. Admin-only endpoint. Frontend: - Add 'BU Lookup' tab to Admin page with search input and results table - Shows BU assignment badge (blue for tagged, amber for untagged) - Supports Enter key to search, loading state, error display - Useful for verifying team assignments when onboarding new users --- backend/routes/ivantiFindings.js | 77 ++++++++++++ frontend/src/components/pages/AdminPage.js | 135 ++++++++++++++++++++- 2 files changed, 211 insertions(+), 1 deletion(-) diff --git a/backend/routes/ivantiFindings.js b/backend/routes/ivantiFindings.js index a4c2db7..72e25f8 100644 --- a/backend/routes/ivantiFindings.js +++ b/backend/routes/ivantiFindings.js @@ -1689,6 +1689,83 @@ function createIvantiFindingsRouter(db, requireAuth) { } }); + /** + * GET /api/ivanti/findings/bu-lookup + * + * Look up the BU assignment for a hostname or IP in the Ivanti API. + * Queries the live Ivanti platform (not the local cache) to discover + * which BU a given host is tagged to. Useful for verifying user team + * assignments before onboarding. + * Requires Admin group. + * + * @query {string} q - Hostname or IP address to look up + * @returns {Object} 200 - { query, findings: [{ hostId, hostName, ipAddress, bu, severity, title }] } + * @returns {Object} 400 - { error } when query is missing + * @returns {Object} 500 - { error } on API failure + */ + router.get('/bu-lookup', requireGroup('Admin'), async (req, res) => { + const q = (req.query.q || '').trim(); + if (!q) return res.status(400).json({ error: 'q parameter is required (hostname or IP)' }); + + try { + const clientId = process.env.IVANTI_CLIENT_ID || '1550'; + const apiKey = process.env.IVANTI_API_KEY; + if (!apiKey) return res.status(503).json({ error: 'Ivanti API key not configured' }); + + // Search by hostname or IP + const filters = [ + { + field: q.match(/^\d+\./) ? 'host.ipAddress' : 'host.hostName', + operator: 'WILDCARD', + value: `*${q}*`, + exclusive: false, + orWithPrevious: false, + implicitFilters: [], + caseSensitive: false + } + ]; + + const result = await ivantiPost(`/client/${clientId}/hostFinding/search`, { + filters, + projection: 'detail', + sort: [{ field: 'severity', direction: 'DESC' }], + page: 0, + size: 20 + }); + + if (!result.ok) { + return res.status(502).json({ error: 'Ivanti API request failed', status: result.status }); + } + + const data = JSON.parse(result.body); + const records = data._embedded?.records || []; + + // Extract BU info from each finding + const findings = records.map(r => ({ + hostId: r.host?.hostId || null, + hostName: r.host?.hostName || null, + ipAddress: r.host?.ipAddress || null, + bu: r.assetCustomAttributes?.['1550_host_1']?.[0] || 'Untagged', + severity: r.severity || null, + title: r.title || null, + })); + + // Deduplicate by hostId+bu + const seen = new Set(); + const unique = findings.filter(f => { + const key = `${f.hostId}-${f.bu}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + + res.json({ query: q, total: data.page?.totalElements || 0, findings: unique }); + } catch (err) { + console.error('[Ivanti] BU lookup error:', err.message); + res.status(500).json({ error: 'BU lookup failed' }); + } + }); + return router; } diff --git a/frontend/src/components/pages/AdminPage.js b/frontend/src/components/pages/AdminPage.js index 15aabd6..0ffc5df 100644 --- a/frontend/src/components/pages/AdminPage.js +++ b/frontend/src/components/pages/AdminPage.js @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText } from 'lucide-react'; +import { Shield, Clock, Activity, Plus, Edit2, Trash2, Loader, AlertCircle, CheckCircle, X, ChevronLeft, ChevronRight, Search, Users, FileText, Globe } from 'lucide-react'; import { useAuth } from '../../contexts/AuthContext'; import ConfirmModal from '../ConfirmModal'; @@ -8,6 +8,7 @@ const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const TABS = [ { id: 'users', label: 'User Management', icon: Shield }, { id: 'audit', label: 'Audit Log', icon: Clock }, + { id: 'bu-lookup', label: 'BU Lookup', icon: Globe }, { id: 'system', label: 'System Info', icon: Activity }, ]; @@ -987,6 +988,128 @@ function AuditLogPanel() { ); } +// --------------------------------------------------------------------------- +// BULookupPanel — query Ivanti API to discover which BU a host belongs to +// --------------------------------------------------------------------------- +function BULookupPanel() { + const [query, setQuery] = useState(''); + const [results, setResults] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSearch = async () => { + const q = query.trim(); + if (!q) return; + setLoading(true); + setError(null); + setResults(null); + try { + const res = await fetch(`${API_BASE}/ivanti/findings/bu-lookup?q=${encodeURIComponent(q)}`, { credentials: 'include' }); + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || `HTTP ${res.status}`); + } + const data = await res.json(); + setResults(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+

+ BU Lookup +

+

+ Search the Ivanti API by hostname or IP to discover which BU a host is assigned to. Use this to verify team assignments for new users. +

+ +
+ setQuery(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleSearch(); }} + placeholder="Enter hostname or IP address..." + style={{ + flex: 1, padding: '0.5rem 0.75rem', + background: 'rgba(15, 23, 42, 0.6)', + border: '1px solid rgba(14, 165, 233, 0.25)', + borderRadius: '0.375rem', color: '#E2E8F0', + fontSize: '0.85rem', fontFamily: 'monospace', outline: 'none', + }} + /> + +
+ + {error && ( +
+ {error} +
+ )} + + {results && ( +
+
+ Found {results.total.toLocaleString()} finding(s) matching "{results.query}" — showing {results.findings.length} unique hosts: +
+ {results.findings.length === 0 ? ( +
+ No findings found for this hostname/IP in Ivanti. +
+ ) : ( + + + + + + + + + + + {results.findings.map((f, i) => ( + + + + + + + ))} + +
HostnameIP AddressBU AssignmentHost ID
{f.hostName || '—'}{f.ipAddress || '—'} + + {f.bu} + + {f.hostId || '—'}
+ )} +
+ )} +
+ ); +} + // --------------------------------------------------------------------------- // SystemInfoPanel // --------------------------------------------------------------------------- @@ -1396,6 +1519,16 @@ export default function AdminPage() { )} + + {activeTab === 'bu-lookup' && ( +
+ {/* ⚠️ CONVENTION: BULookupPanel is referenced but never defined or imported — this will throw a ReferenceError at runtime. Define the component in this file or import it. */} + +
+ )} ); }