diff --git a/src/context/DeviceContext.tsx b/src/context/DeviceContext.tsx index cd53b9b..9f0b6fa 100644 --- a/src/context/DeviceContext.tsx +++ b/src/context/DeviceContext.tsx @@ -33,16 +33,89 @@ interface InstalledApp { type RawRecord = Record; -const pick = (source: RawRecord | null | undefined, keys: string[]): unknown => { +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)) { - return 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; @@ -94,33 +167,71 @@ const normalizeDrive = (driveInput: unknown, index: number): DriveInfo | null => const drive = driveInput as RawRecord; const name = toNonEmptyString( - pick(drive, ['name', 'driveName', 'label', 'volume']) ?? `Drive ${index + 1}`, + 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(pick(drive, ['driveType', 'drive_type', 'type']), 'Unknown'); + const driveType = toNonEmptyString( + resolveValue(drive, ['driveType', 'drive_type', 'type'], [ + 'driveType', + 'drive_type', + 'type', + 'drivetype', + 'mediatype', + 'busType', + ]), + 'Unknown' + ); const totalSizeGB = parseSizeToGB( - pick(drive, [ - 'totalSizeGB', - 'total_size_gb', - 'totalSize', - 'total_size', - 'size', - 'capacity', - 'totalBytes', - 'total_bytes', - ]) + 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( - pick(drive, [ - 'freeSpaceGB', - 'free_space_gb', - 'freeSpace', - 'free_space', - 'free', - 'freeBytes', - 'free_bytes', - 'availableBytes', - ]) + 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; @@ -138,12 +249,17 @@ const extractDrives = (rawInput: unknown): DriveInfo[] => { 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']), + 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) { @@ -155,8 +271,26 @@ const extractDrives = (rawInput: unknown): DriveInfo[] => { } const aggregatedDrive = { - totalSize: pick(raw, ['total_disk_space', 'totalDiskSpace', 'total_storage']), - freeSpace: pick(raw, ['free_disk_space', 'freeDiskSpace', 'available_storage']), + 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) { @@ -181,19 +315,45 @@ const normalizeIpAddress = (entryInput: unknown, index: number): IpAddress | nul const entry = entryInput as RawRecord; const ipAddress = toNonEmptyString( - pick(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6']) ?? '', + resolveValue(entry, ['ipAddress', 'ip_address', 'address', 'ip', 'ipv4', 'ipv6'], [ + 'ipAddress', + 'ip_address', + 'address', + 'ip', + 'ipv4', + 'ipv6', + 'primary_ip', + 'primaryIp', + 'lan_ip', + ]) ?? '', '' ); const interfaceName = toNonEmptyString( - pick(entry, ['interfaceName', 'interface_name', 'adapter', 'name', 'nic']) ?? '', + 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( - pick(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress']) + resolveValue(entry, ['macAddress', 'mac_address', 'mac', 'physicalAddress'], [ + 'macAddress', + 'mac_address', + 'mac', + 'physicalAddress', + 'physical_address', + 'hw_address', + ]) ); return { @@ -208,11 +368,14 @@ const extractIpAddresses = (rawInput: unknown): IpAddress[] => { const raw = rawInput as RawRecord; const potentialArrays = [ - pick(raw, ['ipAddresses']), - pick(raw, ['ip_addresses']), - pick(raw, ['networkInterfaces']), - pick(raw, ['network_interfaces']), - pick(raw, ['interfaces']), + 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) { @@ -223,14 +386,14 @@ const extractIpAddresses = (rawInput: unknown): IpAddress[] => { } } - const singleIp = pick(raw, ['ipAddress', 'ip_address']); + const singleIp = resolveValue(raw, ['ipAddress', 'ip_address', 'primary_ip', 'primaryIp']); if (singleIp) { return [ normalizeIpAddress( { ipAddress: singleIp, - interfaceName: pick(raw, ['interfaceName', 'interface_name', 'adapter']), - macAddress: pick(raw, ['macAddress', 'mac_address']), + interfaceName: resolveValue(raw, ['interfaceName', 'interface_name', 'adapter']), + macAddress: resolveValue(raw, ['macAddress', 'mac_address']), }, 0 ) as IpAddress, @@ -244,13 +407,34 @@ 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']) ?? '', ''); + 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(pick(app, ['app_version', 'version']) ?? '', ''), - publisher: toNonEmptyString(pick(app, ['publisher', 'manufacturer', 'vendor']) ?? '', ''), + 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', + ]) ?? '', + '' + ), }; }; @@ -259,11 +443,14 @@ const extractInstalledApps = (rawInput: unknown): InstalledApp[] => { const raw = rawInput as RawRecord; const potentialArrays = [ - pick(raw, ['installedApplications']), - pick(raw, ['installed_applications']), - pick(raw, ['software']), - pick(raw, ['applications']), - pick(raw, ['apps']), + 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) { @@ -279,18 +466,32 @@ const extractInstalledApps = (rawInput: unknown): InstalledApp[] => { 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']); + 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(pick(record, ['macAddress', 'mac_address', 'mac'])); + const macAddress = toOptionalString( + resolveValue(record, ['macAddress', 'mac_address', 'mac'], [ + 'macAddress', + 'mac_address', + 'mac', + 'physicalAddress', + 'hw_address', + ]) + ); if (!macAddress) return null; return { interfaceName: toNonEmptyString( - pick(record, ['interfaceName', 'interface_name', 'adapter']) ?? `Interface ${idx + 1}`, + resolveValue(record, ['interfaceName', 'interface_name', 'adapter'], [ + 'interfaceName', + 'interface_name', + 'adapter', + 'network_adapter', + 'name', + ]) ?? `Interface ${idx + 1}`, `Interface ${idx + 1}` ), macAddress: macAddress.trim(), @@ -329,47 +530,159 @@ const normalizeDevice = (rawInput: unknown): DetailedDevice => { rawInput && typeof rawInput === 'object' ? (rawInput as RawRecord) : {}; const deviceId = Number( - pick(raw, ['deviceId', 'device_id', 'id', 'deviceID', 'device_id_pk']) ?? 0 + 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 = pick(raw, ['client']); + const clientEntry = resolveValue(raw, ['client'], ['client', 'clientInfo', 'customer']); const clientNameFromNested = clientEntry && typeof clientEntry === 'object' - ? pick(clientEntry as RawRecord, ['name']) + ? resolveValue(clientEntry as RawRecord, ['name', 'clientName', 'client_name']) : undefined; const hostname = toNonEmptyString( - pick(raw, ['hostname', 'hostName', 'computerName', 'deviceName']) ?? `Device ${deviceId}`, + resolveValue(raw, ['hostname', 'hostName', 'computerName', 'deviceName'], [ + 'hostname', + 'hostName', + 'computerName', + 'deviceName', + 'host', + 'machineName', + ]) ?? `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']) ?? '', + osName: toNonEmptyString( + resolveValue( + raw, + ['osName', 'os_name', 'operatingSystem', 'os'], + [ + 'osName', + 'os_name', + 'operatingSystem', + 'operating_system', + 'operatingSystemName', + 'operating_system_name', + 'osFriendlyName', + 'os_caption', + 'oscaption', + 'os', + ] + ) ?? '', '' ), - gpuNames: splitGpuNames(pick(raw, ['gpuNames', 'gpu_names', 'gpu_name', 'gpus'])), + 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( - pick(raw, ['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB']) ?? '', + resolveValue( + raw, + ['totalMemory', 'total_memory', 'memory', 'totalMemoryMb', 'totalMemoryMB'], + [ + 'totalMemory', + 'total_memory', + 'memory', + 'totalMemoryMb', + 'totalMemoryMB', + 'memory_total', + 'ram_total', + 'physicalMemory', + ] + ) ?? '', '' ), lastBootTime: toNonEmptyString( - pick(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot']) ?? '', + resolveValue(raw, ['lastBootTime', 'last_boot_time', 'bootTime', 'lastBoot', 'lastBootUpTime'], [ + 'lastBootTime', + 'last_boot_time', + 'bootTime', + 'lastBoot', + 'lastBootUpTime', + 'last_boot', + 'boot_time', + ]) ?? '', '' ), lastCheckedIn: toNonEmptyString( - pick(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at']) ?? '', + resolveValue(raw, ['lastCheckedIn', 'last_checked_in', 'lastSeen', 'checked_in_at'], [ + 'lastCheckedIn', + 'last_checked_in', + 'lastSeen', + 'checked_in_at', + 'lastSeenAt', + 'last_seen', + ]) ?? '', '' ), drives, @@ -378,7 +691,9 @@ const normalizeDevice = (rawInput: unknown): DetailedDevice => { installedApplications: extractInstalledApps(raw), clientName: toOptionalString( - pick(raw, ['clientName', 'client_name']) ?? clientNameFromNested ?? pick(raw, ['clientIdentifier']) + resolveValue(raw, ['clientName', 'client_name'], ['clientName', 'client_name', 'client']) ?? + clientNameFromNested ?? + resolveValue(raw, ['clientIdentifier', 'client_identifier']) ) ?? undefined, }; };