Added new backfill button for CVE sync.
Some checks failed
Deploy Frontend / deploy (push) Failing after 36s

This commit is contained in:
Bailey Taylor
2025-10-08 09:46:24 +08:00
parent 67334a65d8
commit dcaab71244

View File

@@ -1,396 +1,463 @@
// src/components/admin/AdminControlsPanel.tsx // src/components/admin/AdminControlsPanel.tsx
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Typography, Typography,
Box, Box,
Button, Button,
Alert, Alert,
CircularProgress, CircularProgress,
Snackbar, Snackbar,
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
Select, Select,
MenuItem, MenuItem,
FormControl, FormControl,
InputLabel, InputLabel,
} from '@mui/material'; DialogContentText,
import { SwitchTextTrack } from '@/components/SwitchTextTrack'; } from '@mui/material';
import CVELogStream from '@/components/CVELogStream'; import { SwitchTextTrack } from '@/components/SwitchTextTrack';
import api from '@/lib/axios'; import CVELogStream from '@/components/CVELogStream';
import api from '@/lib/axios';
export default function AdminControlsPanel() { export default function AdminControlsPanel() {
const [pingEnabled, setPingEnabled] = useState<boolean | null>(null); const [pingEnabled, setPingEnabled] = useState<boolean | null>(null);
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [logOutput, setLogOutput] = useState(''); const [logOutput, setLogOutput] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [toastOpen, setToastOpen] = useState(false); const [toastOpen, setToastOpen] = useState(false);
const [toastMessage, setToastMessage] = useState(''); const [toastMessage, setToastMessage] = useState('');
const [clientList, setClientList] = useState<Array<{clientId: number;clientIdentifier: string;clientName: string;}>>([]); const [clientList, setClientList] = useState<Array<{clientId: number;clientIdentifier: string;clientName: string;}>>([]);
const [selectedClientId, setSelectedClientId] = useState<number | null>(null); const [selectedClientId, setSelectedClientId] = useState<number | null>(null);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [cveVisible, setCveVisible] = useState(false); const [backfillDialogOpen, setBackfillDialogOpen] = useState(false);
const [kevVisible, setKevVisible] = useState(false); const [cveVisible, setCveVisible] = useState(false);
const [msrcVisible, setMsrcVisible] = useState(false); const [kevVisible, setKevVisible] = useState(false);
const [msrcVisible, setMsrcVisible] = useState(false);
useEffect(() => { useEffect(() => {
const fetchInitialState = async () => { const fetchInitialState = async () => {
try { try {
const res = await api.get("/system/ping-status"); const res = await api.get("/system/ping-status");
if (res?.data?.acceptPings !== undefined) { if (res?.data?.acceptPings !== undefined) {
setPingEnabled(res.data.acceptPings); setPingEnabled(res.data.acceptPings);
}
} catch (err) {
console.error("❌ Failed to fetch ping status", err);
setPingEnabled(false);
} }
};
fetchInitialState();
}, []);
const togglePing = async (enabled: boolean) => {
try {
const res = await api.post(`/admin/toggle-ping?enabled=${enabled}`);
setMessage(res.data);
setPingEnabled(enabled);
} catch (err) { } catch (err) {
console.error("❌ Failed to fetch ping status", err); console.error("Toggle failed:", err);
setPingEnabled(false); setMessage("Failed to update ping status");
} }
}; };
fetchInitialState();
}, []);
const togglePing = async (enabled: boolean) => {
try {
const res = await api.post(`/admin/toggle-ping?enabled=${enabled}`);
setMessage(res.data);
setPingEnabled(enabled);
} catch (err) {
console.error("Toggle failed:", err);
setMessage("Failed to update ping status");
}
};
useEffect(() => {
if (!drawerOpen) return;
useEffect(() => { const interval = setInterval(async () => {
if (!drawerOpen) return; const res = await api.get('/admin/scripts/fetch-cve/logs');
setLogOutput(res.data);
}, 3000);
const interval = setInterval(async () => { return () => clearInterval(interval);
const res = await api.get('/admin/scripts/fetch-cve/logs'); }, [drawerOpen]);
setLogOutput(res.data);
}, 3000);
return () => clearInterval(interval); const fetchClients = async () => {
}, [drawerOpen]); try {
const res = await api.get('/auth/clients');
setClientList(res.data);
} catch (err) {
console.error('Failed to fetch clients', err);
}
};
const fetchClients = async () => { const runCveSync = async () => {
try { try {
const res = await api.get('/auth/clients'); // Adjust endpoint if needed setLoading(true);
setClientList(res.data); // Expected: [{ clientId, name }] await api.post('/admin/scripts/fetch-cve');
} catch (err) { setCveVisible(true);
console.error('Failed to fetch clients', err); } catch (err: any) {
} alert("❌ Failed to start CVE sync: " + (err?.response?.data || err.message));
}; } finally {
setLoading(false);
}
};
const runCveSync = async () => { const runCveBackfill = async () => {
try { setBackfillDialogOpen(false);
setLoading(true); try {
await api.post('/admin/scripts/fetch-cve'); setLoading(true);
setCveVisible(true); // 👈 Spawn CVE console await api.post('/admin/scripts/fetch-cve-backfill');
} catch (err: any) { setCveVisible(true);
alert("❌ Failed to start CVE sync: " + (err?.response?.data || err.message)); setToastMessage('⏳ CVE backfill started - this will take 20-30 hours to complete!');
} finally { setToastOpen(true);
setLoading(false); } catch (err: any) {
} setToastMessage("❌ Failed to start CVE backfill: " + (err?.response?.data || err.message));
}; setToastOpen(true);
const runKevSync = async () => { } finally {
try { setLoading(false);
setLoading(true); }
await api.post('/admin/scripts/fetch-kev'); };
setKevVisible(true); // 👈 Spawn KEV console
setToastMessage('✅ KEV sync started.');
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to sync KEVs: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const runMsrcSync = async () => { const runKevSync = async () => {
try { try {
setLoading(true); setLoading(true);
await api.post('/admin/scripts/fetch-msrc'); await api.post('/admin/scripts/fetch-kev');
setMsrcVisible(true); // 👈 Spawn MSRC console setKevVisible(true);
setToastMessage('✅ MSRC sync started.'); setToastMessage('✅ KEV sync started.');
setToastOpen(true); setToastOpen(true);
} catch (err: any) { } catch (err: any) {
setToastMessage("❌ Failed to fetch Microsoft CVEs: " + (err?.response?.data || err.message)); setToastMessage("❌ Failed to sync KEVs: " + (err?.response?.data || err.message));
setToastOpen(true); setToastOpen(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const runVulnCacheRefresh = async () => { const runMsrcSync = async () => {
try { try {
setLoading(true); setLoading(true);
const res = await api.post('/admin/vulns/refresh-cache'); await api.post('/admin/scripts/fetch-msrc');
setToastMessage(res.data); // Show backend result in toast setMsrcVisible(true);
setToastOpen(true); setToastMessage('✅ MSRC sync started.');
} catch (err: any) { setToastOpen(true);
setToastMessage("❌ Failed to refresh vulnerability cache: " + (err?.response?.data || err.message)); } catch (err: any) {
setToastOpen(true); setToastMessage("❌ Failed to fetch Microsoft CVEs: " + (err?.response?.data || err.message));
} finally { setToastOpen(true);
setLoading(false); } finally {
} setLoading(false);
}; }
};
const refreshSoftwareCache = async () => { const runVulnCacheRefresh = async () => {
try { try {
setLoading(true); setLoading(true);
const res = await api.post('/admin/software/refresh-cache'); const res = await api.post('/admin/vulns/refresh-cache');
setToastMessage(res.data); setToastMessage(res.data);
setToastOpen(true); setToastOpen(true);
} catch (err: any) { } catch (err: any) {
setToastMessage("❌ Failed to refresh installed software cache: " + (err?.response?.data || err.message)); setToastMessage("❌ Failed to refresh vulnerability cache: " + (err?.response?.data || err.message));
setToastOpen(true); setToastOpen(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const refreshStatistics = async () => { const refreshSoftwareCache = async () => {
try { try {
setLoading(true); setLoading(true);
await api.post('/admin/statistics/refresh'); const res = await api.post('/admin/software/refresh-cache');
setToastMessage('✅ CVE Statistics refreshed.'); setToastMessage(res.data);
setToastOpen(true); setToastOpen(true);
} catch (err: any) { } catch (err: any) {
setToastMessage("❌ Failed to refresh statistics: " + (err?.response?.data || err.message)); setToastMessage("❌ Failed to refresh installed software cache: " + (err?.response?.data || err.message));
setToastOpen(true); setToastOpen(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const normalizeSoftware = async () => { const refreshStatistics = async () => {
try { try {
setLoading(true); setLoading(true);
const res = await api.post('/admin/software/normalize'); await api.post('/admin/statistics/refresh');
setToastMessage(res.data); // ✅ show result setToastMessage('✅ CVE Statistics refreshed.');
setToastOpen(true); setToastOpen(true);
} catch (err: any) { } catch (err: any) {
setToastMessage("❌ Failed to normalize software entries: " + (err?.response?.data || err.message)); setToastMessage("❌ Failed to refresh statistics: " + (err?.response?.data || err.message));
setToastOpen(true); setToastOpen(true);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const generateDemoDevice = async () => { const normalizeSoftware = async () => {
const clientId = prompt("Enter clientId for the demo device:"); try {
setLoading(true);
const res = await api.post('/admin/software/normalize');
setToastMessage(res.data);
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to normalize software entries: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
if (!clientId) return; const generateDemoDevice = async () => {
const clientId = prompt("Enter clientId for the demo device:");
try { if (!clientId) return;
setLoading(true);
const res = await api.post('/system/devices/demo', {
clientId: Number(clientId)
});
setToastMessage(`✅ Demo device created: ${res.data.deviceId}`);
} catch (err: any) {
setToastMessage("❌ Failed to generate demo device: " + (err?.response?.data || err.message));
} finally {
setLoading(false);
setToastOpen(true);
}
};
const openDialog = async () => { try {
await fetchClients(); setLoading(true);
setDialogOpen(true); const res = await api.post('/system/devices/demo', {
}; clientId: Number(clientId)
});
setToastMessage(`✅ Demo device created: ${res.data.deviceId}`);
} catch (err: any) {
setToastMessage("❌ Failed to generate demo device: " + (err?.response?.data || err.message));
} finally {
setLoading(false);
setToastOpen(true);
}
};
return ( const openDialog = async () => {
<Box sx={{ p: 4 }}> await fetchClients();
<Typography variant="h5" gutterBottom>Admin Controls</Typography> setDialogOpen(true);
};
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}> return (
<Typography>Accept pings:</Typography> <Box sx={{ p: 4 }}>
{pingEnabled === null ? ( <Typography variant="h5" gutterBottom>Admin Controls</Typography>
<CircularProgress size={20} />
) : (
<SwitchTextTrack
checked={pingEnabled}
disabled={pingEnabled === null}
onChange={(e) => togglePing(e.target.checked)}
/>
)}
</Box>
{message && <Alert severity="info" sx={{ mb: 2 }}>{message}</Alert>} <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<Typography>Accept pings:</Typography>
{pingEnabled === null ? (
<CircularProgress size={20} />
) : (
<SwitchTextTrack
checked={pingEnabled}
disabled={pingEnabled === null}
onChange={(e) => togglePing(e.target.checked)}
/>
)}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}> {message && <Alert severity="info" sx={{ mb: 2 }}>{message}</Alert>}
<Button
variant="contained"
onClick={runCveSync}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Running Sync...' : 'Run CVE Sync'}
</Button>
<Button <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}>
variant="contained"
onClick={runVulnCacheRefresh}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Refreshing Cache...' : 'Recheck Device Vulnerabilities'}
</Button>
<Button
variant="contained"
onClick={refreshSoftwareCache}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Refreshing Software Cache...' : 'Recheck Installed Software'}
</Button>
<Button
variant="contained"
onClick={normalizeSoftware}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Normalizing...' : 'Normalize Installed Software'}
</Button>
<Button
variant="contained"
onClick={runKevSync}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Syncing KEVs...' : 'Import CISA KEVs'}
</Button>
<Button
variant="contained"
onClick={runMsrcSync}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Syncing MSRC...' : 'Import Microsoft CVEs'}
</Button>
<Button
variant="contained"
onClick={refreshStatistics}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Refreshing Statistics...' : 'Refresh CVE Statistics'}
</Button>
<Button
variant="contained"
color="secondary"
onClick={openDialog}
sx={{ mt: 2 }}
>
Add Demo Device
</Button>
</Box>
<CVELogStream
visible={cveVisible}
title="🔍 CVE Sync Logs"
streamUrl="/admin/scripts/fetch-cve/logs/stream"
clearUrl="/admin/scripts/fetch-cve/clear-logs"
/>
<CVELogStream
visible={kevVisible}
title="🛡️ KEV Sync Logs"
streamUrl="/admin/scripts/fetch-kev/logs/stream"
clearUrl="/admin/scripts/fetch-kev/clear-logs"
/>
<CVELogStream
visible={msrcVisible}
title="🖥️ MSRC Sync Logs"
streamUrl="/admin/scripts/fetch-msrc/logs/stream"
clearUrl="/admin/scripts/fetch-msrc/clear-logs"
/>
<Snackbar
open={toastOpen}
autoHideDuration={6000}
onClose={() => setToastOpen(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setToastOpen(false)}
severity="info"
sx={{ width: '100%' }}
>
{toastMessage}
</Alert>
</Snackbar>
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>Select a Client</DialogTitle>
<DialogContent>
<FormControl fullWidth sx={{ mt: 2 }}>
<InputLabel id="client-select-label">Client</InputLabel>
<Select
labelId="client-select-label"
value={selectedClientId ?? ''}
onChange={(e) => setSelectedClientId(Number(e.target.value))}
label="Client"
>
{clientList.map((client) => (
<MenuItem key={client.clientId} value={client.clientId}>
{client.clientName} ({client.clientIdentifier})
</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button <Button
variant="contained" variant="contained"
disabled={!selectedClientId || loading} onClick={runCveSync}
onClick={async () => { disabled={loading}
try { sx={{ mt: 2 }}
setLoading(true);
const res = await api.post('/system/devices/demo', {
clientId: selectedClientId,
});
setToastMessage(`✅ Demo device created: ${res.data.deviceId}`);
setToastOpen(true);
setDialogOpen(false);
} catch (err: any) {
setToastMessage("❌ Failed to create demo device: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
}}
> >
Create {loading ? 'Running Sync...' : 'Run CVE Sync (Last 30 Days)'}
</Button> </Button>
</DialogActions>
</Dialog> <Button
variant="contained"
color="warning"
onClick={() => setBackfillDialogOpen(true)}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Starting Backfill...' : 'Backfill All CVEs (2002-Present)'}
</Button>
<Button
variant="contained"
onClick={runVulnCacheRefresh}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Refreshing Cache...' : 'Recheck Device Vulnerabilities'}
</Button>
<Button
variant="contained"
onClick={refreshSoftwareCache}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Refreshing Software Cache...' : 'Recheck Installed Software'}
</Button>
<Button
variant="contained"
onClick={normalizeSoftware}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Normalizing...' : 'Normalize Installed Software'}
</Button>
<Button
variant="contained"
onClick={runKevSync}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Syncing KEVs...' : 'Import CISA KEVs'}
</Button>
<Button
variant="contained"
onClick={runMsrcSync}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Syncing MSRC...' : 'Import Microsoft CVEs'}
</Button>
<Button
variant="contained"
onClick={refreshStatistics}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Refreshing Statistics...' : 'Refresh CVE Statistics'}
</Button>
<Button
variant="contained"
color="secondary"
onClick={openDialog}
sx={{ mt: 2 }}
>
Add Demo Device
</Button>
</Box>
);
} </Box>
<CVELogStream
visible={cveVisible}
title="🔍 CVE Sync Logs"
streamUrl="/admin/scripts/fetch-cve/logs/stream"
clearUrl="/admin/scripts/fetch-cve/clear-logs"
/>
<CVELogStream
visible={kevVisible}
title="🛡️ KEV Sync Logs"
streamUrl="/admin/scripts/fetch-kev/logs/stream"
clearUrl="/admin/scripts/fetch-kev/clear-logs"
/>
<CVELogStream
visible={msrcVisible}
title="🖥️ MSRC Sync Logs"
streamUrl="/admin/scripts/fetch-msrc/logs/stream"
clearUrl="/admin/scripts/fetch-msrc/clear-logs"
/>
<Snackbar
open={toastOpen}
autoHideDuration={6000}
onClose={() => setToastOpen(false)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={() => setToastOpen(false)}
severity="info"
sx={{ width: '100%' }}
>
{toastMessage}
</Alert>
</Snackbar>
{/* Backfill Confirmation Dialog */}
<Dialog
open={backfillDialogOpen}
onClose={() => setBackfillDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle> Confirm CVE Backfill</DialogTitle>
<DialogContent>
<DialogContentText>
<strong>This will download ALL CVEs from 2002 to present (~250,000 CVEs).</strong>
<br /><br />
Expected runtime: <strong>20-30 hours</strong>
<br />
API calls: <strong>~8,000-10,000 requests</strong>
<br /><br />
The process is resumable - if it stops, you can restart it and it will continue from where it left off.
<br /><br />
Are you sure you want to proceed?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setBackfillDialogOpen(false)}>
Cancel
</Button>
<Button
variant="contained"
color="warning"
onClick={runCveBackfill}
disabled={loading}
>
Start Backfill
</Button>
</DialogActions>
</Dialog>
{/* Demo Device Dialog */}
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>Select a Client</DialogTitle>
<DialogContent>
<FormControl fullWidth sx={{ mt: 2 }}>
<InputLabel id="client-select-label">Client</InputLabel>
<Select
labelId="client-select-label"
value={selectedClientId ?? ''}
onChange={(e) => setSelectedClientId(Number(e.target.value))}
label="Client"
>
{clientList.map((client) => (
<MenuItem key={client.clientId} value={client.clientId}>
{client.clientName} ({client.clientIdentifier})
</MenuItem>
))}
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button
variant="contained"
disabled={!selectedClientId || loading}
onClick={async () => {
try {
setLoading(true);
const res = await api.post('/system/devices/demo', {
clientId: selectedClientId,
});
setToastMessage(`✅ Demo device created: ${res.data.deviceId}`);
setToastOpen(true);
setDialogOpen(false);
} catch (err: any) {
setToastMessage("❌ Failed to create demo device: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
}}
>
Create
</Button>
</DialogActions>
</Dialog>
</Box>
);
}