Add BU Lookup tool to Admin panel
Backend: - GET /api/ivanti/findings/bu-lookup?q=<hostname|ip> — 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
This commit is contained in:
@@ -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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
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 { useAuth } from '../../contexts/AuthContext';
|
||||||
import ConfirmModal from '../ConfirmModal';
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
|
|||||||
const TABS = [
|
const TABS = [
|
||||||
{ id: 'users', label: 'User Management', icon: Shield },
|
{ id: 'users', label: 'User Management', icon: Shield },
|
||||||
{ id: 'audit', label: 'Audit Log', icon: Clock },
|
{ id: 'audit', label: 'Audit Log', icon: Clock },
|
||||||
|
{ id: 'bu-lookup', label: 'BU Lookup', icon: Globe },
|
||||||
{ id: 'system', label: 'System Info', icon: Activity },
|
{ 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 (
|
||||||
|
<div>
|
||||||
|
<h3 style={{ fontFamily: 'monospace', fontSize: '1rem', fontWeight: 700, color: '#0EA5E9', textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '0.5rem' }}>
|
||||||
|
BU Lookup
|
||||||
|
</h3>
|
||||||
|
<p style={{ fontSize: '0.8rem', color: '#94A3B8', marginBottom: '1rem' }}>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1rem' }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
onChange={e => 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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={loading || !query.trim()}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem', borderRadius: '0.375rem',
|
||||||
|
background: 'rgba(14, 165, 233, 0.15)',
|
||||||
|
border: '1px solid rgba(14, 165, 233, 0.4)',
|
||||||
|
color: '#0EA5E9', fontSize: '0.8rem', fontWeight: 600,
|
||||||
|
fontFamily: 'monospace', cursor: 'pointer',
|
||||||
|
opacity: loading || !query.trim() ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Searching...' : 'Lookup'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{ padding: '0.75rem', background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)', borderRadius: '0.375rem', color: '#FCA5A5', fontSize: '0.8rem', marginBottom: '1rem' }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{results && (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: '#64748B', marginBottom: '0.75rem' }}>
|
||||||
|
Found {results.total.toLocaleString()} finding(s) matching "{results.query}" — showing {results.findings.length} unique hosts:
|
||||||
|
</div>
|
||||||
|
{results.findings.length === 0 ? (
|
||||||
|
<div style={{ padding: '1rem', textAlign: 'center', color: '#475569', fontSize: '0.85rem' }}>
|
||||||
|
No findings found for this hostname/IP in Ivanti.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '1px solid rgba(14,165,233,0.2)' }}>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>Hostname</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>IP Address</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>BU Assignment</th>
|
||||||
|
<th style={{ textAlign: 'left', padding: '0.5rem', color: '#0EA5E9', fontWeight: 600 }}>Host ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{results.findings.map((f, i) => (
|
||||||
|
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.03)' }}>
|
||||||
|
<td style={{ padding: '0.5rem', color: '#CBD5E1', fontFamily: 'monospace' }}>{f.hostName || '—'}</td>
|
||||||
|
<td style={{ padding: '0.5rem', color: '#CBD5E1', fontFamily: 'monospace' }}>{f.ipAddress || '—'}</td>
|
||||||
|
<td style={{ padding: '0.5rem' }}>
|
||||||
|
<span style={{
|
||||||
|
padding: '0.15rem 0.5rem', borderRadius: '0.25rem', fontSize: '0.7rem', fontWeight: 600, fontFamily: 'monospace',
|
||||||
|
background: f.bu === 'Untagged' ? 'rgba(245,158,11,0.15)' : 'rgba(14,165,233,0.15)',
|
||||||
|
border: f.bu === 'Untagged' ? '1px solid rgba(245,158,11,0.3)' : '1px solid rgba(14,165,233,0.3)',
|
||||||
|
color: f.bu === 'Untagged' ? '#FCD34D' : '#7DD3FC',
|
||||||
|
}}>
|
||||||
|
{f.bu}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.5rem', color: '#64748B', fontFamily: 'monospace', fontSize: '0.75rem' }}>{f.hostId || '—'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// SystemInfoPanel
|
// SystemInfoPanel
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1396,6 +1519,16 @@ export default function AdminPage() {
|
|||||||
<SystemInfoPanel />
|
<SystemInfoPanel />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'bu-lookup' && (
|
||||||
|
<div
|
||||||
|
className="intel-card"
|
||||||
|
style={{ borderRadius: '0.5rem', padding: '1.25rem' }}
|
||||||
|
>
|
||||||
|
{/* ⚠️ 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. */}
|
||||||
|
<BULookupPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user