From f7ecef695c9ea92c0b64556cd2556afe4d3fac19 Mon Sep 17 00:00:00 2001 From: Bailey Taylor Date: Wed, 29 Oct 2025 12:33:12 +0800 Subject: [PATCH] First iterative "fix" with Codex CLI, reintroducing the device details when viewing devices. --- src/context/DeviceContext.tsx | 490 +++++++++++++++++++++++++++++----- src/types/devices.ts | 11 +- 2 files changed, 430 insertions(+), 71 deletions(-) diff --git a/src/context/DeviceContext.tsx b/src/context/DeviceContext.tsx index c3c4413..cd53b9b 100644 --- a/src/context/DeviceContext.tsx +++ b/src/context/DeviceContext.tsx @@ -1,20 +1,12 @@ 'use client'; import React, { createContext, useContext, useState, useEffect } from 'react'; -import api from '@/lib/axios'; +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(); - -// Helper function to convert UTC timestamps to local time -const convertUtcToLocal = (utcString: string | null): string | null => { - if (!utcString) return null; - // Add 'Z' to indicate it's UTC, then convert to local time - return new Date(utcString + 'Z').toLocaleString(); -}; - - +import { DetailedDevice } from '@/types/devices'; +import { disableConsoleInProd } from '@/lib/disableConsole'; +disableConsoleInProd(); interface DriveInfo { name: string; @@ -32,13 +24,365 @@ interface MacAddress { interfaceName: string; macAddress: string; } - -interface InstalledApp { - app_name: string; - app_version: string; - publisher: string; -} - + +interface InstalledApp { + app_name: string; + app_version: string; + publisher: string; +} + +type RawRecord = Record; + +const pick = (source: RawRecord | null | undefined, keys: string[]): unknown => { + if (!source) return undefined; + for (const key of keys) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + return source[key]; + } + } + 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( + pick(drive, ['name', 'driveName', 'label', 'volume']) ?? `Drive ${index + 1}`, + `Drive ${index + 1}` + ); + const driveType = toNonEmptyString(pick(drive, ['driveType', 'drive_type', 'type']), 'Unknown'); + const totalSizeGB = parseSizeToGB( + pick(drive, [ + 'totalSizeGB', + 'total_size_gb', + 'totalSize', + 'total_size', + 'size', + 'capacity', + 'totalBytes', + 'total_bytes', + ]) + ); + const freeSpaceGB = parseSizeToGB( + pick(drive, [ + 'freeSpaceGB', + 'free_space_gb', + 'freeSpace', + 'free_space', + 'free', + 'freeBytes', + 'free_bytes', + 'availableBytes', + ]) + ); + + 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 = [ + pick(raw, ['drives']), + pick(raw, ['driveInfo']), + pick(raw, ['drive_info']), + pick(raw, ['storageDevices']), + pick(raw, ['drive_details']), + pick(raw, ['storage']), + ]; + + 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: pick(raw, ['total_disk_space', 'totalDiskSpace', 'total_storage']), + freeSpace: pick(raw, ['free_disk_space', 'freeDiskSpace', 'available_storage']), + }; + + 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( + pick(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6']) ?? '', + '' + ); + + const interfaceName = toNonEmptyString( + pick(entry, ['interfaceName', 'interface_name', 'adapter', 'name', 'nic']) ?? '', + ipAddress ? `Interface ${index + 1}` : '' + ); + + if (!ipAddress && !interfaceName) return null; + + const macAddress = toOptionalString( + pick(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress']) + ); + + 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 = [ + pick(raw, ['ipAddresses']), + pick(raw, ['ip_addresses']), + pick(raw, ['networkInterfaces']), + pick(raw, ['network_interfaces']), + pick(raw, ['interfaces']), + ]; + + 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 = pick(raw, ['ipAddress', 'ip_address']); + if (singleIp) { + return [ + normalizeIpAddress( + { + ipAddress: singleIp, + interfaceName: pick(raw, ['interfaceName', 'interface_name', 'adapter']), + macAddress: pick(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(pick(app, ['app_name', 'name', 'title']) ?? '', ''); + if (!appName) return null; + + return { + app_name: appName, + app_version: toNonEmptyString(pick(app, ['app_version', 'version']) ?? '', ''), + publisher: toNonEmptyString(pick(app, ['publisher', 'manufacturer', 'vendor']) ?? '', ''), + }; +}; + +const extractInstalledApps = (rawInput: unknown): InstalledApp[] => { + if (!rawInput || typeof rawInput !== 'object') return []; + const raw = rawInput as RawRecord; + + const potentialArrays = [ + pick(raw, ['installedApplications']), + pick(raw, ['installed_applications']), + pick(raw, ['software']), + pick(raw, ['applications']), + pick(raw, ['apps']), + ]; + + 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 = pick(raw, ['macAddresses', 'mac_addresses']); + + 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(pick(record, ['macAddress', 'mac_address', 'mac'])); + if (!macAddress) return null; + return { + interfaceName: toNonEmptyString( + pick(record, ['interfaceName', 'interface_name', 'adapter']) ?? `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( + pick(raw, ['deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk']) ?? 0 + ); + + const drives = extractDrives(raw); + const ipAddresses = extractIpAddresses(raw); + const macAddresses = extractMacAddresses(raw, ipAddresses); + const clientEntry = pick(raw, ['client']); + const clientNameFromNested = + clientEntry && typeof clientEntry === 'object' + ? pick(clientEntry as RawRecord, ['name']) + : undefined; + + const hostname = toNonEmptyString( + pick(raw, ['hostname', 'hostName', 'computerName', 'deviceName']) ?? `Device ${deviceId}`, + deviceId ? `Device ${deviceId}` : 'Unknown Device' + ); + + return { + deviceId, + hostname, + osName: toNonEmptyString(pick(raw, ['osName', 'os_name', 'operatingSystem', 'os']) ?? '', ''), + osVersion: toNonEmptyString(pick(raw, ['osVersion', 'os_version', 'osVersionString']) ?? '', ''), + windowsVersion: toNonEmptyString(pick(raw, ['windowsVersion', 'windows_version', 'osRelease']) ?? '', ''), + windowsBuild: toNonEmptyString(pick(raw, ['windowsBuild', 'windows_build', 'buildNumber']) ?? '', ''), + osArchitecture: toNonEmptyString(pick(raw, ['osArchitecture', 'os_architecture', 'architecture']) ?? '', ''), + processorName: toNonEmptyString(pick(raw, ['processorName', 'processor_name', 'cpuName']) ?? '', ''), + processorArchitecture: toNonEmptyString( + pick(raw, ['processorArchitecture', 'processor_architecture', 'cpuArchitecture']) ?? '', + '' + ), + gpuNames: splitGpuNames(pick(raw, ['gpuNames', 'gpu_names', 'gpu_name', 'gpus'])), + totalMemory: toNonEmptyString( + pick(raw, ['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB']) ?? '', + '' + ), + lastBootTime: toNonEmptyString( + pick(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot']) ?? '', + '' + ), + lastCheckedIn: toNonEmptyString( + pick(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at']) ?? '', + '' + ), + drives, + ipAddresses, + macAddresses, + installedApplications: extractInstalledApps(raw), + clientName: + toOptionalString( + pick(raw, ['clientName', 'client_name']) ?? clientNameFromNested ?? pick(raw, ['clientIdentifier']) + ) ?? undefined, + }; +}; + interface DeviceVulnerability { cveId: string; @@ -49,16 +393,21 @@ interface DeviceVulnerability { lastModifiedDate: string; } -interface CachedSoftwareEntry { - id: number; - softwareName: string; - hostname: string; - version: string; - deviceId: number; - totalCves: number; - lastUpdated: 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[] }; @@ -93,10 +442,12 @@ export const DeviceProvider = ({ vulnerabilitiesByDevice: { [deviceId: string]: DeviceVulnerability[] }; }; }) => { - const [devices, setDevices] = useState(() => { - if (initialData?.devices?.length) return initialData.devices; - return []; - }); + 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 ?? {} @@ -123,28 +474,32 @@ export const DeviceProvider = ({ console.group('๐Ÿ“ก Fetching Devices, Vulnerabilities, and Software'); - const [devicesRes, softwareRes] = await Promise.all([ - api.get(devicesEndpoint, { withCredentials: true }), - api.get(softwareEndpoint, { withCredentials: true }), - ]); + 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) - setDevices(devicesRes.data.devices); - setDeviceVulns(devicesRes.data.vulnerabilitiesByDevice); - setCachedSoftware(softwareRes.data); + 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) as DeviceVulnerability[][]).forEach((vulns) => { - vulns.forEach((vuln) => { - if (vuln.cveId) { - uniqueCveIds.add(vuln.cveId); - } - }); + Object.values(devicesRes.data?.vulnerabilitiesByDevice ?? {}).forEach((vulns) => { + vulns.forEach((vuln) => { + if (vuln.cveId) { + uniqueCveIds.add(vuln.cveId); + } + }); }); // STEP 3: Fetch detailed CVE data @@ -171,35 +526,38 @@ const cveDetailsRes = await api.get('/vuln/cves/lookup', setDetailedCveLookup(lookupMap); } - } catch (error: any) { - console.group('โŒ Device fetch failed'); - console.error('๐Ÿ”ด Axios Error Message:', error.message); - console.error('๐Ÿงพ Axios Error Config:', error.config); - if (error.response) { - console.error('๐Ÿ“‰ Status:', error.response.status); - console.error('๐Ÿ“„ Response Headers:', error.response.headers); - console.error('๐Ÿ“„ Response Data:', error.response.data); - } else if (error.request) { - console.error('๐Ÿ›‘ No response received:', error.request); - } - console.groupEnd(); - } finally { - setLoading(false); - } + } 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]); +}, [devices.length, initialData, username, authLoading, roles]); useEffect(() => { console.log("๐Ÿง  Devices updated:", devices); }, [devices]); - if (typeof window !== 'undefined') { - (window as any).__debug_devices = devices; - } + if (typeof window !== 'undefined') { + (window as Window & { __debug_devices?: DetailedDevice[] }).__debug_devices = devices; + } return (