'use client'; import React, { createContext, useContext, useState, useEffect } from 'react'; import api from '@/lib/axios'; import type { AxiosError } from 'axios'; import { useAuth } from '@/context/AuthContext'; import { DetailedDevice } from '@/types/devices'; import { disableConsoleInProd } from '@/lib/disableConsole'; disableConsoleInProd(); interface DriveInfo { name: string; driveType: string; totalSizeGB: number; freeSpaceGB: number; } interface IpAddress { interfaceName: string; ipAddress: string; } interface MacAddress { interfaceName: string; macAddress: string; } interface InstalledApp { app_name: string; app_version: string; publisher: string; } type RawRecord = Record; const normalizeKey = (key: string): string => key.toLowerCase().replace(/[^a-z0-9]/g, ''); const getDirectValue = (source: RawRecord | null | undefined, keys: string[]): unknown => { if (!source) return undefined; for (const key of keys) { if (Object.prototype.hasOwnProperty.call(source, key)) { const value = source[key]; if (value !== undefined && value !== null) { return value; } } } return undefined; }; const findValueDeep = ( source: unknown, normalizedTargets: string[], seen = new WeakSet() ): unknown => { if (!source || typeof source !== 'object') return undefined; const stack: unknown[] = [source]; while (stack.length > 0) { const current = stack.pop(); if (!current || typeof current !== 'object') continue; if (seen.has(current as object)) continue; seen.add(current as object); if (Array.isArray(current)) { for (const item of current) { if (item && (typeof item === 'object' || typeof item === 'string')) { stack.push(item); } } continue; } for (const [key, value] of Object.entries(current as RawRecord)) { const normalizedKey = normalizeKey(key); if (normalizedTargets.includes(normalizedKey) && value !== undefined && value !== null) { return value; } if (value && typeof value === 'object') { stack.push(value); } } } return undefined; }; const resolveValue = ( source: RawRecord | null | undefined, keys: string[], fallbackKeys: string[] = keys ): unknown => { if (!source) return undefined; const direct = getDirectValue(source, keys); if (direct !== undefined && direct !== null) { return direct; } const normalizedTargets = Array.from(new Set(fallbackKeys.map(normalizeKey))).filter(Boolean); return findValueDeep(source, normalizedTargets); }; const resolveArray = ( source: RawRecord | null | undefined, keys: string[], fallbackKeys?: string[] ): T[] | undefined => { const value = resolveValue(source, keys, fallbackKeys ?? keys); if (Array.isArray(value)) { return value as T[]; } return undefined; }; const toOptionalString = (value: unknown): string | undefined => { if (value === null || value === undefined) return undefined; if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); return undefined; }; const toNonEmptyString = (value: unknown, fallback = ''): string => { const candidate = toOptionalString(value)?.trim(); return candidate && candidate.length > 0 ? candidate : fallback; }; const parseSizeToGB = (value: unknown): number => { if (value === null || value === undefined) return 0; const normalizeNumeric = (num: number): number => { if (!Number.isFinite(num)) return 0; if (num > 1024 ** 3) return +(num / 1024 ** 3).toFixed(2); // bytes if (num > 1024 ** 2) return +(num / 1024 ** 2).toFixed(2); // KB if (num > 1024) return +(num / 1024).toFixed(2); // MB return +num.toFixed(2); // already GB }; if (typeof value === 'number') { return normalizeNumeric(value); } if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) return 0; const numeric = parseFloat(trimmed.replace(/[^\d.\-]/g, '')); if (!Number.isFinite(numeric)) return 0; if (/tb/i.test(trimmed)) return +(numeric * 1024).toFixed(2); if (/gb/i.test(trimmed)) return +numeric.toFixed(2); if (/mb/i.test(trimmed)) return +(numeric / 1024).toFixed(2); if (/kb/i.test(trimmed)) return +(numeric / (1024 ** 2)).toFixed(2); if (/bytes?/i.test(trimmed)) return +(numeric / (1024 ** 3)).toFixed(2); return normalizeNumeric(numeric); } return 0; }; const normalizeDrive = (driveInput: unknown, index: number): DriveInfo | null => { if (!driveInput || typeof driveInput !== 'object') return null; const drive = driveInput as RawRecord; const name = toNonEmptyString( resolveValue(drive, ['name', 'driveName', 'label', 'volume'], [ 'name', 'driveName', 'label', 'volume', 'drive_letter', 'driveletter', 'mount_point', 'mountpoint', 'logicaldisk', 'deviceid', ]) ?? `Drive ${index + 1}`, `Drive ${index + 1}` ); const driveType = toNonEmptyString( resolveValue(drive, ['driveType', 'drive_type', 'type'], [ 'driveType', 'drive_type', 'type', 'drivetype', 'mediatype', 'busType', ]), 'Unknown' ); const totalSizeGB = parseSizeToGB( resolveValue( drive, ['totalSizeGB', 'total_size_gb', 'totalSize', 'total_size', 'size', 'capacity', 'totalBytes', 'total_bytes'], [ 'totalSizeGB', 'total_size_gb', 'totalSize', 'total_size', 'size', 'capacity', 'totalBytes', 'total_bytes', 'size_bytes', 'capacity_bytes', 'totalsize', 'totalspace', 'totalspacebytes', ] ) ); const freeSpaceGB = parseSizeToGB( resolveValue( drive, ['freeSpaceGB', 'free_space_gb', 'freeSpace', 'free_space', 'free', 'freeBytes', 'free_bytes', 'availableBytes'], [ 'freeSpaceGB', 'free_space_gb', 'freeSpace', 'free_space', 'free', 'freeBytes', 'free_bytes', 'availableBytes', 'available_bytes', 'freespace', 'freespacebytes', 'remaining', ] ) ); if (!name && !totalSizeGB && !freeSpaceGB) return null; return { name, driveType, totalSizeGB, freeSpaceGB, }; }; const extractDrives = (rawInput: unknown): DriveInfo[] => { if (!rawInput || typeof rawInput !== 'object') return []; const raw = rawInput as RawRecord; const potentialArrays = [ resolveArray(raw, ['drives']), resolveArray(raw, ['driveInfo']), resolveArray(raw, ['drive_info']), resolveArray(raw, ['storageDevices']), resolveArray(raw, ['drive_details']), resolveArray(raw, ['storage']), resolveArray(raw, ['logicalDrives']), resolveArray(raw, ['logical_drives']), resolveArray(raw, ['volumes']), resolveArray(raw, ['volume_info']), resolveArray(raw, ['disks']), ]; for (const candidate of potentialArrays) { if (Array.isArray(candidate)) { return candidate .map((drive, idx: number) => normalizeDrive(drive, idx)) .filter((drive): drive is DriveInfo => Boolean(drive)); } } const aggregatedDrive = { totalSize: resolveValue(raw, ['total_disk_space', 'totalDiskSpace', 'total_storage'], [ 'total_disk_space', 'totalDiskSpace', 'total_storage', 'totalSpace', 'total_space', 'storage_total', 'disk_total', ]), freeSpace: resolveValue(raw, ['free_disk_space', 'freeDiskSpace', 'available_storage'], [ 'free_disk_space', 'freeDiskSpace', 'available_storage', 'free_storage', 'freeSpace', 'free_space', 'availableSpace', 'available_space', 'storage_free', ]), }; if (aggregatedDrive.totalSize || aggregatedDrive.freeSpace) { const normalized = normalizeDrive( { name: 'System', driveType: 'Unknown', totalSizeGB: aggregatedDrive.totalSize, freeSpaceGB: aggregatedDrive.freeSpace, }, 0 ); return normalized ? [normalized] : []; } return []; }; const normalizeIpAddress = (entryInput: unknown, index: number): IpAddress | null => { if (!entryInput || typeof entryInput !== 'object') return null; const entry = entryInput as RawRecord; const ipAddress = toNonEmptyString( resolveValue(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6'], [ 'ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6', 'primary_ip', 'primaryIp', 'lan_ip', ]) ?? '', '' ); const interfaceName = toNonEmptyString( resolveValue(entry, ['interfaceName', 'interface_name', 'adapter', 'name', 'nic'], [ 'interfaceName', 'interface_name', 'adapter', 'name', 'nic', 'network_adapter', 'interface', 'description', ]) ?? '', ipAddress ? `Interface ${index + 1}` : '' ); if (!ipAddress && !interfaceName) return null; const macAddress = toOptionalString( resolveValue(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress'], [ 'macAddress', 'mac_address', 'mac', 'physicalAddress', 'physical_address', 'hw_address', ]) ); return { interfaceName: interfaceName || `Interface ${index + 1}`, ipAddress: ipAddress || 'Unknown', macAddress: macAddress?.trim() || undefined, }; }; const extractIpAddresses = (rawInput: unknown): IpAddress[] => { if (!rawInput || typeof rawInput !== 'object') return []; const raw = rawInput as RawRecord; const potentialArrays = [ resolveArray(raw, ['ipAddresses']), resolveArray(raw, ['ip_addresses']), resolveArray(raw, ['networkInterfaces']), resolveArray(raw, ['network_interfaces']), resolveArray(raw, ['interfaces']), resolveArray(raw, ['networkAdapters']), resolveArray(raw, ['network_adapters']), resolveArray(raw, ['network']), ]; for (const candidate of potentialArrays) { if (Array.isArray(candidate)) { return candidate .map((entry, idx: number) => normalizeIpAddress(entry, idx)) .filter((entry): entry is IpAddress => Boolean(entry)); } } const singleIp = resolveValue(raw, ['ipAddress', 'ip_address', 'primary_ip', 'primaryIp']); if (singleIp) { return [ normalizeIpAddress( { ipAddress: singleIp, interfaceName: resolveValue(raw, ['interfaceName', 'interface_name', 'adapter']), macAddress: resolveValue(raw, ['macAddress', 'mac_address']), }, 0 ) as IpAddress, ].filter(Boolean); } return []; }; const normalizeInstalledApp = (appInput: unknown): InstalledApp | null => { if (!appInput || typeof appInput !== 'object') return null; const app = appInput as RawRecord; const appName = toNonEmptyString( resolveValue(app, ['app_name', 'name', 'title'], [ 'app_name', 'name', 'title', 'display_name', 'product_name', ]) ?? '', '' ); if (!appName) return null; return { app_name: appName, app_version: toNonEmptyString( resolveValue(app, ['app_version', 'version'], ['app_version', 'version', 'display_version', 'product_version']) ?? '', '' ), publisher: toNonEmptyString( resolveValue(app, ['publisher', 'manufacturer', 'vendor'], [ 'publisher', 'manufacturer', 'vendor', 'company', 'publisher_name', ]) ?? '', '' ), }; }; const extractInstalledApps = (rawInput: unknown): InstalledApp[] => { if (!rawInput || typeof rawInput !== 'object') return []; const raw = rawInput as RawRecord; const potentialArrays = [ resolveArray(raw, ['installedApplications']), resolveArray(raw, ['installed_applications']), resolveArray(raw, ['software']), resolveArray(raw, ['applications']), resolveArray(raw, ['apps']), resolveArray(raw, ['installedPrograms']), resolveArray(raw, ['installed_programs']), resolveArray(raw, ['programs']), ]; for (const candidate of potentialArrays) { if (Array.isArray(candidate)) { return candidate .map((app) => normalizeInstalledApp(app)) .filter((app): app is InstalledApp => Boolean(app)); } } return []; }; const extractMacAddresses = (rawInput: unknown, ipAddresses: IpAddress[]): MacAddress[] => { if (!rawInput || typeof rawInput !== 'object') return []; const raw = rawInput as RawRecord; const potentialArrays = resolveArray(raw, ['macAddresses', 'mac_addresses', 'macs', 'mac_list']); if (Array.isArray(potentialArrays)) { return potentialArrays .map((entry, idx: number) => { if (!entry || typeof entry !== 'object') return null; const record = entry as RawRecord; const macAddress = toOptionalString( resolveValue(record, ['macAddress', 'mac_address', 'mac'], [ 'macAddress', 'mac_address', 'mac', 'physicalAddress', 'hw_address', ]) ); if (!macAddress) return null; return { interfaceName: toNonEmptyString( resolveValue(record, ['interfaceName', 'interface_name', 'adapter'], [ 'interfaceName', 'interface_name', 'adapter', 'network_adapter', 'name', ]) ?? `Interface ${idx + 1}`, `Interface ${idx + 1}` ), macAddress: macAddress.trim(), }; }) .filter((entry): entry is MacAddress => Boolean(entry)); } return ipAddresses .filter((entry) => Boolean(entry.macAddress)) .map((entry) => ({ interfaceName: entry.interfaceName, macAddress: entry.macAddress as string, })); }; const splitGpuNames = (value: unknown): string[] => { if (Array.isArray(value)) { return value .map((item) => toNonEmptyString(item, '')) .filter((item) => item.length > 0); } if (typeof value === 'string') { return value .split(/[,;\n]+/) .map((item) => item.trim()) .filter((item) => item.length > 0); } return []; }; const normalizeDevice = (rawInput: unknown): DetailedDevice => { const raw: RawRecord = rawInput && typeof rawInput === 'object' ? (rawInput as RawRecord) : {}; const deviceId = Number( resolveValue(raw, ['deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk'], [ 'deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk', ]) ?? 0 ); const drives = extractDrives(raw); const ipAddresses = extractIpAddresses(raw); const macAddresses = extractMacAddresses(raw, ipAddresses); const clientEntry = resolveValue(raw, ['client'], ['client', 'clientInfo', 'customer']); const clientNameFromNested = clientEntry && typeof clientEntry === 'object' ? resolveValue(clientEntry as RawRecord, ['name', 'clientName', 'client_name']) : undefined; const hostname = toNonEmptyString( resolveValue(raw, ['hostname', 'hostName', 'computerName', 'deviceName'], [ 'hostname', 'hostName', 'computerName', 'deviceName', 'host', 'machineName', ]) ?? `Device ${deviceId}`, deviceId ? `Device ${deviceId}` : 'Unknown Device' ); return { deviceId, hostname, osName: toNonEmptyString( resolveValue( raw, ['osName', 'os_name', 'operatingSystem', 'os'], [ 'osName', 'os_name', 'operatingSystem', 'operating_system', 'operatingSystemName', 'operating_system_name', 'osFriendlyName', 'os_caption', 'oscaption', 'os', ] ) ?? '', '' ), osVersion: toNonEmptyString( resolveValue(raw, ['osVersion', 'os_version', 'osVersionString'], [ 'osVersion', 'os_version', 'osVersionString', 'operatingSystemVersion', 'osversion', ]) ?? '', '' ), windowsVersion: toNonEmptyString( resolveValue(raw, ['windowsVersion', 'windows_version', 'osRelease'], [ 'windowsVersion', 'windows_version', 'osRelease', 'windowsRelease', 'windows_release', ]) ?? '', '' ), windowsBuild: toNonEmptyString( resolveValue(raw, ['windowsBuild', 'windows_build', 'buildNumber'], [ 'windowsBuild', 'windows_build', 'buildNumber', 'build_number', 'windowsBuildNumber', ]) ?? '', '' ), osArchitecture: toNonEmptyString( resolveValue(raw, ['osArchitecture', 'os_architecture', 'architecture'], [ 'osArchitecture', 'os_architecture', 'architecture', 'platform', 'osarch', ]) ?? '', '' ), processorName: toNonEmptyString( resolveValue(raw, ['processorName', 'processor_name', 'cpuName'], [ 'processorName', 'processor_name', 'cpuName', 'cpu_name', 'processor', ]) ?? '', '' ), processorArchitecture: toNonEmptyString( resolveValue(raw, ['processorArchitecture', 'processor_architecture', 'cpuArchitecture'], [ 'processorArchitecture', 'processor_architecture', 'cpuArchitecture', 'cpu_architecture', 'cpuArch', ]) ?? '', '' ), gpuNames: splitGpuNames( resolveValue(raw, ['gpuNames', 'gpu_names', 'gpu_name', 'gpus', 'graphics']) ?? [] ), totalMemory: toNonEmptyString( resolveValue( raw, ['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB'], [ 'totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB', 'memory_total', 'ram_total', 'physicalMemory', ] ) ?? '', '' ), lastBootTime: toNonEmptyString( resolveValue(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot', 'lastBootUpTime'], [ 'lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot', 'lastBootUpTime', 'last_boot', 'boot_time', ]) ?? '', '' ), lastCheckedIn: toNonEmptyString( resolveValue(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at'], [ 'lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at', 'lastSeenAt', 'last_seen', ]) ?? '', '' ), drives, ipAddresses, macAddresses, installedApplications: extractInstalledApps(raw), clientName: toOptionalString( resolveValue(raw, ['clientName', 'client_name'], ['clientName', 'client_name', 'client']) ?? clientNameFromNested ?? resolveValue(raw, ['clientIdentifier', 'client_identifier']) ) ?? undefined, }; }; interface DeviceVulnerability { cveId: string; title: string; severity: string; score?: number; publishedDate: string; lastModifiedDate: string; } interface CachedSoftwareEntry { id: number; softwareName: string; hostname: string; version: string; deviceId: number; totalCves: number; lastUpdated: string; } interface RawDevicesResponse { devices?: unknown[]; vulnerabilitiesByDevice?: { [deviceId: string]: DeviceVulnerability[] }; } interface DeviceContextType { devices: DetailedDevice[]; deviceVulns: { [deviceId: string]: DeviceVulnerability[] }; cachedSoftware: CachedSoftwareEntry[]; detailedCveLookup: { [cveId: string]: DeviceVulnerability }; // ๐Ÿ”ฅ New setDevices: React.Dispatch>; setDeviceVulns: React.Dispatch>; setCachedSoftware: React.Dispatch>; setDetailedCveLookup: React.Dispatch>; loading: boolean; } const DeviceContext = createContext(undefined); export const useDeviceContext = () => { const context = useContext(DeviceContext); if (!context) { throw new Error('useDeviceContext must be used within a DeviceProvider'); } return context; }; export const DeviceProvider = ({ children, initialData, }: { children: React.ReactNode; initialData?: { devices: DetailedDevice[]; vulnerabilitiesByDevice: { [deviceId: string]: DeviceVulnerability[] }; }; }) => { const [devices, setDevices] = useState(() => { if (initialData?.devices?.length) { return initialData.devices.map((device) => normalizeDevice(device)); } return []; }); const [deviceVulns, setDeviceVulns] = useState<{ [deviceId: string]: DeviceVulnerability[] }>( initialData?.vulnerabilitiesByDevice ?? {} ); const [detailedCveLookup, setDetailedCveLookup] = useState<{ [cveId: string]: DeviceVulnerability }>({}); const [cachedSoftware, setCachedSoftware] = useState([]); // โฌ…๏ธ NEW const [loading, setLoading] = useState(!initialData); const { username, roles, loading: authLoading } = useAuth(); useEffect(() => { if (devices.length > 0 || initialData || authLoading || !username) return; const fetchDevicesVulnsAndSoftware = async () => { try { const isAdmin = roles?.includes('ADMIN'); const devicesEndpoint = isAdmin ? '/cached/devices/with-vulns/all' : '/cached/devices/with-vulns'; const softwareEndpoint = '/cached/software/summary'; console.group('๐Ÿ“ก Fetching Devices, Vulnerabilities, and Software'); const [devicesRes, softwareRes] = await Promise.all([ api.get(devicesEndpoint, { withCredentials: true }), api.get(softwareEndpoint, { withCredentials: true }), ]); console.log('โœ… Devices fetched:', devicesRes.data); console.log('โœ… Software fetched:', softwareRes.data); console.groupEnd(); // STEP 1: Set basic data (keep original UTC timestamps for calculations) const normalizedDevices = Array.isArray(devicesRes.data?.devices) ? (devicesRes.data.devices as unknown[]).map((device) => normalizeDevice(device)) : []; setDevices(normalizedDevices); setDeviceVulns(devicesRes.data?.vulnerabilitiesByDevice ?? {}); setCachedSoftware(softwareRes.data ?? []); // STEP 2: Extract all unique CVE IDs const uniqueCveIds = new Set(); Object.values(devicesRes.data?.vulnerabilitiesByDevice ?? {}).forEach((vulns) => { vulns.forEach((vuln) => { if (vuln.cveId) { uniqueCveIds.add(vuln.cveId); } }); }); // STEP 3: Fetch detailed CVE data if (uniqueCveIds.size > 0) { console.log('๐Ÿ”Ž Fetching detailed CVEs for:', Array.from(uniqueCveIds)); const params = new URLSearchParams(); Array.from(uniqueCveIds).forEach(cveId => { params.append('cveIds', cveId); }); const cveDetailsRes = await api.get('/vuln/cves/lookup', { params, withCredentials: true, }); const lookupMap: { [cveId: string]: DeviceVulnerability } = {}; cveDetailsRes.data.forEach((cve) => { lookupMap[cve.cveId] = cve; }); setDetailedCveLookup(lookupMap); } } catch (error) { console.group('โŒ Device fetch failed'); const axiosError = error as AxiosError; console.error('๐Ÿ”ด Axios Error Message:', axiosError.message); console.error('๐Ÿงพ Axios Error Config:', axiosError.config); if (axiosError.response) { console.error('๐Ÿ“‰ Status:', axiosError.response.status); console.error('๐Ÿ“„ Response Headers:', axiosError.response.headers); console.error('๐Ÿ“„ Response Data:', axiosError.response.data); } else if (axiosError.request) { console.error('๐Ÿ›‘ No response received:', axiosError.request); } else { console.error('โ“ Unexpected error:', error); } console.groupEnd(); } finally { setLoading(false); } }; fetchDevicesVulnsAndSoftware(); }, [devices.length, initialData, username, authLoading, roles]); useEffect(() => { console.log("๐Ÿง  Devices updated:", devices); }, [devices]); if (typeof window !== 'undefined') { (window as Window & { __debug_devices?: DetailedDevice[] }).__debug_devices = devices; } return ( {children} ); };