Files
ld-sysinfo-frontend/src/components/admin/AdminControlsPanel.tsx
Bailey Taylor 7cbe4e4b7b
Some checks failed
Deploy Frontend / deploy (push) Failing after 33s
New Github API support for verifying CVE counts.
2025-10-08 10:40:24 +08:00

488 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/components/admin/AdminControlsPanel.tsx
'use client';
import { useEffect, useState } from 'react';
import {
Typography,
Box,
Button,
Alert,
CircularProgress,
Snackbar,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Select,
MenuItem,
FormControl,
InputLabel,
DialogContentText,
} from '@mui/material';
import { SwitchTextTrack } from '@/components/SwitchTextTrack';
import CVELogStream from '@/components/CVELogStream';
import api from '@/lib/axios';
export default function AdminControlsPanel() {
const [pingEnabled, setPingEnabled] = useState<boolean | null>(null);
const [message, setMessage] = useState<string>('');
const [drawerOpen, setDrawerOpen] = useState(false);
const [logOutput, setLogOutput] = useState('');
const [loading, setLoading] = useState(false);
const [toastOpen, setToastOpen] = useState(false);
const [toastMessage, setToastMessage] = useState('');
const [clientList, setClientList] = useState<Array<{clientId: number;clientIdentifier: string;clientName: string;}>>([]);
const [selectedClientId, setSelectedClientId] = useState<number | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [backfillDialogOpen, setBackfillDialogOpen] = useState(false);
const [cveVisible, setCveVisible] = useState(false);
const [kevVisible, setKevVisible] = useState(false);
const [msrcVisible, setMsrcVisible] = useState(false);
useEffect(() => {
const fetchInitialState = async () => {
try {
const res = await api.get("/system/ping-status");
if (res?.data?.acceptPings !== undefined) {
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) {
console.error("Toggle failed:", err);
setMessage("Failed to update ping status");
}
};
useEffect(() => {
if (!drawerOpen) return;
const interval = setInterval(async () => {
const res = await api.get('/admin/scripts/fetch-cve/logs');
setLogOutput(res.data);
}, 3000);
return () => clearInterval(interval);
}, [drawerOpen]);
const fetchClients = async () => {
try {
const res = await api.get('/auth/clients');
setClientList(res.data);
} catch (err) {
console.error('Failed to fetch clients', err);
}
};
const runCveSync = async () => {
try {
setLoading(true);
await api.post('/admin/scripts/fetch-cve');
setCveVisible(true);
} catch (err: any) {
alert("❌ Failed to start CVE sync: " + (err?.response?.data || err.message));
} finally {
setLoading(false);
}
};
const runCveBackfill = async () => {
setBackfillDialogOpen(false);
try {
setLoading(true);
await api.post('/admin/scripts/fetch-cve-backfill');
setCveVisible(true);
setToastMessage('⏳ CVE backfill started - this will take 20-30 hours to complete!');
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to start CVE backfill: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const runKevSync = async () => {
try {
setLoading(true);
await api.post('/admin/scripts/fetch-kev');
setKevVisible(true);
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 () => {
try {
setLoading(true);
await api.post('/admin/scripts/fetch-msrc');
setMsrcVisible(true);
setToastMessage('✅ MSRC sync started.');
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to fetch Microsoft CVEs: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const runVulnCacheRefresh = async () => {
try {
setLoading(true);
const res = await api.post('/admin/vulns/refresh-cache');
setToastMessage(res.data);
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to refresh vulnerability cache: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const refreshSoftwareCache = async () => {
try {
setLoading(true);
const res = await api.post('/admin/software/refresh-cache');
setToastMessage(res.data);
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to refresh installed software cache: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const refreshStatistics = async () => {
try {
setLoading(true);
await api.post('/admin/statistics/refresh');
setToastMessage('✅ CVE Statistics refreshed.');
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to refresh statistics: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const normalizeSoftware = async () => {
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);
}
};
const generateDemoDevice = async () => {
const clientId = prompt("Enter clientId for the demo device:");
if (!clientId) return;
try {
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 runCveVerification = async () => {
try {
setLoading(true);
await api.post('/admin/scripts/verify-cve-count');
setCveVisible(true);
setToastMessage('🔍 CVE verification started - check logs for comparison results');
setToastOpen(true);
} catch (err: any) {
setToastMessage("❌ Failed to start CVE verification: " + (err?.response?.data || err.message));
setToastOpen(true);
} finally {
setLoading(false);
}
};
const openDialog = async () => {
await fetchClients();
setDialogOpen(true);
};
return (
<Box sx={{ p: 4 }}>
<Typography variant="h5" gutterBottom>Admin Controls</Typography>
<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>
{message && <Alert severity="info" sx={{ mb: 2 }}>{message}</Alert>}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 3 }}>
<Button
variant="contained"
onClick={runCveSync}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Running Sync...' : 'Run CVE Sync (Last 30 Days)'}
</Button>
<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"
color="info"
onClick={runCveVerification}
disabled={loading}
sx={{ mt: 2 }}
>
{loading ? 'Verifying...' : 'Verify CVE Count (GitHub API)'}
</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>
<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>
);
}