'use client'; import React, { useEffect, useState } from 'react'; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Collapse, IconButton, Typography, Box, CircularProgress, TextField, Dialog, DialogTitle, DialogContent, DialogActions, Button,Tooltip } from '@mui/material'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { FixedSizeList as List } from 'react-window'; import { useDeviceContext } from '@/context/DeviceContext'; import api from '@/lib/axios'; interface CachedSoftwareEntry { id: number; deviceId: number; hostname: string; softwareName: string; appVersion: string; publisher: string; lastUpdated: string; totalCves: number; cveList: string; } interface DeviceVulnerability { cveId: string; title: string; severity: string; score?: number; publishedDate: string; lastModifiedDate: string; } interface GroupedSoftwareEntry { softwareName: string; totalDevices: number; instances: CachedSoftwareEntry[]; } export default function SoftwarePage() { const [softwareList, setSoftwareList] = useState([]); const [expandedIndex, setExpandedIndex] = useState(null); const [loading, setLoading] = useState(false); const [drawerPageStates, setDrawerPageStates] = useState>({}); const [page, setPage] = useState(0); // Top-level page const [pageSize, setPageSize] = useState(10); // Top-level pageSize (default 10) const [openCveDialog, setOpenCveDialog] = useState(false); const [selectedCves, setSelectedCves] = useState([]); const [selectedHostname, setSelectedHostname] = useState(''); const { detailedCveLookup } = useDeviceContext(); const [sortMode, setSortMode] = useState('vulnerabilities_desc'); // Default if you want "vulnerable first" useEffect(() => { const fetchSoftware = async () => { try { setLoading(true); const res = await api.get('/cached/software/summary'); const grouped: { [key: string]: CachedSoftwareEntry[] } = {}; res.data.forEach(entry => { if (!grouped[entry.softwareName]) { grouped[entry.softwareName] = []; } grouped[entry.softwareName].push(entry); }); let formatted = Object.entries(grouped).map(([softwareName, instances]) => ({ softwareName, totalDevices: instances.length, instances, })); // 🔥 Sort: put groups with the most vulnerable instances at the top formatted = formatted.sort((a, b) => { const aVulnerable = a.instances.some(inst => inst.totalCves > 0) ? 1 : 0; const bVulnerable = b.instances.some(inst => inst.totalCves > 0) ? 1 : 0; return bVulnerable - aVulnerable; // Vulnerable groups first }); setSoftwareList(formatted); } catch (err) { console.error('❌ Failed to fetch cached software summary:', err); } finally { setLoading(false); } }; fetchSoftware(); }, []); const toggleRow = (index: number) => { setExpandedIndex(expandedIndex === index ? null : index); // 🔥 Reset page state for that drawer setDrawerPageStates(prev => ({ ...prev, [index]: { page: 0, pageSize: 10 } })); }; return (
Installed Software (Cached) Browse cached installed software across your environment. Sort and explore by device count, name, or vulnerability presence. { setSortMode(e.target.value); setPage(0); setExpandedIndex(null); }} SelectProps={{ native: true }} size="small" sx={{ width: 220 }} > { setPageSize(Number(e.target.value)); setPage(0); }} SelectProps={{ native: true }} size="small" sx={{ width: 120 }} > {[10, 25, 50, 100].map((option) => ( ))} setPage((prev) => Math.max(prev - 1, 0))} disabled={page === 0}> ◀ setPage((prev) => (prev + 1) * pageSize < softwareList.length ? prev + 1 : prev)} disabled={(page + 1) * pageSize >= softwareList.length}> ▶ {loading ? ( ) : ( Software Name Total Devices {[...softwareList] .sort((a, b) => { switch (sortMode) { case 'alpha_asc': return a.softwareName.localeCompare(b.softwareName); case 'alpha_desc': return b.softwareName.localeCompare(a.softwareName); case 'devices_asc': return a.totalDevices - b.totalDevices; case 'devices_desc': return b.totalDevices - a.totalDevices; case 'vulnerabilities_asc': const aVulns = a.instances.reduce((sum, inst) => sum + inst.totalCves, 0); const bVulns = b.instances.reduce((sum, inst) => sum + inst.totalCves, 0); return aVulns - bVulns; case 'vulnerabilities_desc': const aVulnsDesc = a.instances.reduce((sum, inst) => sum + inst.totalCves, 0); const bVulnsDesc = b.instances.reduce((sum, inst) => sum + inst.totalCves, 0); return bVulnsDesc - aVulnsDesc; default: return 0; } }) .slice(page * pageSize, (page + 1) * pageSize) // 🥇 paginate AFTER sorting .map((row, index) => { const globalIndex = index + page * pageSize; // ✅ return ( toggleRow(globalIndex)} // ✅ sx={{ cursor: 'pointer' }} > { e.stopPropagation(); toggleRow(globalIndex); }} sx={{ cursor: 'pointer' }} > { e.stopPropagation(); toggleRow(globalIndex); }} > {expandedIndex === globalIndex ? : } {row.softwareName} {row.totalDevices}
Hostname Version Publisher CVEs Last Updated {row.instances.map((instance) => ( { const cveIds = instance.cveList.split(','); const uniqueCveIds = Array.from(new Set(cveIds)); const fullDetails = uniqueCveIds .map(cveId => { const cve = detailedCveLookup[cveId]; if (cve) { return { cveId: cve.cveId, title: cve.title, severity: cve.severity, score: cve.score, publishedDate: cve.publishedDate ?? '', lastModifiedDate: cve.lastModifiedDate ?? cve.publishedDate ?? '', } as DeviceVulnerability; } return null; }) .filter(Boolean) as DeviceVulnerability[]; setSelectedCves(fullDetails); setSelectedHostname(instance.hostname); setOpenCveDialog(true); }} sx={{ cursor: 'pointer' }} > {instance.hostname} {instance.appVersion} {instance.publisher || 'Unknown Publisher'} {instance.cveList ? ( {Array.from(new Set(instance.cveList.split(','))).length} ) : ( None )} {new Date(instance.lastUpdated).toLocaleDateString()} ))}
); })} )} setOpenCveDialog(false)} fullWidth maxWidth="md"> CVEs for {selectedHostname} {selectedCves.length === 0 ? ( No CVEs found. ) : ( CVE ID Severity Score Description Last Updated {selectedCves.map((cve, idx) => ( {cve.cveId} {cve.severity ? cve.severity.charAt(0).toUpperCase() + cve.severity.slice(1).toLowerCase() : 'Unknown'} {cve.score === -1 || cve.score === undefined ? 'Unknown' : cve.score} {cve.title || 'Unknown'} {new Date(cve.lastModifiedDate).toLocaleDateString()} ))}
)}
); }