Added new backfill button for CVE sync.
Some checks failed
Deploy Frontend / deploy (push) Failing after 36s
Some checks failed
Deploy Frontend / deploy (push) Failing after 36s
This commit is contained in:
@@ -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;
|
|
||||||
|
|
||||||
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'); // Adjust endpoint if needed
|
|
||||||
setClientList(res.data); // Expected: [{ clientId, name }]
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch clients', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const runCveSync = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
await api.post('/admin/scripts/fetch-cve');
|
|
||||||
setCveVisible(true); // 👈 Spawn CVE console
|
|
||||||
} catch (err: any) {
|
|
||||||
alert("❌ Failed to start CVE sync: " + (err?.response?.data || err.message));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const runKevSync = async () => {
|
|
||||||
try {
|
|
||||||
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 () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
await api.post('/admin/scripts/fetch-msrc');
|
|
||||||
setMsrcVisible(true); // 👈 Spawn MSRC console
|
|
||||||
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); // Show backend result in toast
|
|
||||||
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); // ✅ show result
|
|
||||||
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 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'}
|
|
||||||
</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>
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!drawerOpen) return;
|
||||||
|
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
const res = await api.get('/admin/scripts/fetch-cve/logs');
|
||||||
|
setLogOutput(res.data);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
</Box>
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<CVELogStream
|
const runCveBackfill = async () => {
|
||||||
visible={cveVisible}
|
setBackfillDialogOpen(false);
|
||||||
title="🔍 CVE Sync Logs"
|
try {
|
||||||
streamUrl="/admin/scripts/fetch-cve/logs/stream"
|
setLoading(true);
|
||||||
clearUrl="/admin/scripts/fetch-cve/clear-logs"
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<CVELogStream
|
const runKevSync = async () => {
|
||||||
visible={kevVisible}
|
try {
|
||||||
title="🛡️ KEV Sync Logs"
|
setLoading(true);
|
||||||
streamUrl="/admin/scripts/fetch-kev/logs/stream"
|
await api.post('/admin/scripts/fetch-kev');
|
||||||
clearUrl="/admin/scripts/fetch-kev/clear-logs"
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<CVELogStream
|
const runMsrcSync = async () => {
|
||||||
visible={msrcVisible}
|
try {
|
||||||
title="🖥️ MSRC Sync Logs"
|
setLoading(true);
|
||||||
streamUrl="/admin/scripts/fetch-msrc/logs/stream"
|
await api.post('/admin/scripts/fetch-msrc');
|
||||||
clearUrl="/admin/scripts/fetch-msrc/clear-logs"
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
<Snackbar
|
const refreshSoftwareCache = async () => {
|
||||||
open={toastOpen}
|
try {
|
||||||
autoHideDuration={6000}
|
setLoading(true);
|
||||||
onClose={() => setToastOpen(false)}
|
const res = await api.post('/admin/software/refresh-cache');
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
setToastMessage(res.data);
|
||||||
>
|
setToastOpen(true);
|
||||||
<Alert
|
} catch (err: any) {
|
||||||
onClose={() => setToastOpen(false)}
|
setToastMessage("❌ Failed to refresh installed software cache: " + (err?.response?.data || err.message));
|
||||||
severity="info"
|
setToastOpen(true);
|
||||||
sx={{ width: '100%' }}
|
} finally {
|
||||||
>
|
setLoading(false);
|
||||||
{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>
|
const refreshStatistics = async () => {
|
||||||
</FormControl>
|
try {
|
||||||
</DialogContent>
|
setLoading(true);
|
||||||
<DialogActions>
|
await api.post('/admin/statistics/refresh');
|
||||||
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
|
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 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
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user