InventoryAgent/Inventory.Core/SystemInfoCollector.cs
2025-10-21 12:58:33 +08:00

673 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management;
using System.Net;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using Microsoft.Win32; // Added for Registry access
using System.Text;
using System.Text.Json;
namespace Inventory.Core
{
public class SystemInfoCollector
{
public ConsumerType Consumer { get; set; } = ConsumerType.Unknown;
public string GetComputerName()
{
return Environment.MachineName;
}
public string GetDeviceType()
{
if (!OperatingSystem.IsWindows()) return "N/A";
// Using Win32_SystemEnclosure to determine device type
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_SystemEnclosure");
foreach (ManagementObject mo in searcher.Get())
{
foreach (int chassisType in (ushort[])mo["ChassisTypes"])
{
switch (chassisType)
{
case 3: // Desktop
case 4: // Low Profile Desktop
case 5: // Pizza Box
case 6: // Mini Tower
case 7: // Tower
return "Desktop";
case 8: // Portable
case 9: // Laptop
case 10: // Notebook
case 11: // Hand Held
case 12: // Docking Station
case 14: // Sub Notebook
return "Laptop";
case 23: // Rack Mount Chassis
return "Server";
}
}
}
}
catch { /* Ignore WMI errors */ }
// Fallback to checking for a battery
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Battery");
if (searcher.Get().Count > 0)
{
return "Laptop";
}
}
catch { /* Ignore WMI errors */ }
return "Unknown";
}
public string? GetSystemSerialNumber()
{
if (!OperatingSystem.IsWindows()) return "N/A";
// 1. Primary Method: Win32_ComputerSystemProduct is often the most reliable.
string? serial = GetWmiProperty("Win32_ComputerSystemProduct", "IdentifyingNumber");
if (IsValidSerialNumber(serial))
{
return serial;
}
// 2. Fallback Method: Win32_BIOS is a common alternative.
string? biosSerial = GetWmiProperty("Win32_BIOS", "SerialNumber");
if (IsValidSerialNumber(biosSerial))
{
return biosSerial;
}
// 3. If both are invalid, prefer the first non-empty one found, otherwise return "N/A".
return !string.IsNullOrWhiteSpace(serial) ? serial : biosSerial ?? "N/A";
}
public string? GetMotherboardSerialNumber()
{
if (!OperatingSystem.IsWindows()) return "N/A";
string? serial = GetWmiProperty("Win32_BaseBoard", "SerialNumber");
return IsValidSerialNumber(serial) ? serial : "N/A";
}
public string? GetSystemUUID()
{
if (!OperatingSystem.IsWindows()) return "N/A";
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT UUID FROM Win32_ComputerSystemProduct");
ManagementObject? mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
return mo?["UUID"]?.ToString()?.Trim();
}
catch { return "N/A"; }
}
public string? GetProcessor()
{
if (!OperatingSystem.IsWindows()) return "N/A";
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_Processor");
ManagementObject? mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
return mo?["Name"]?.ToString()?.Trim();
}
catch { return "N/A"; }
}
public string GetTotalRAM()
{
if (!OperatingSystem.IsWindows()) return "N/A";
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
ManagementObject? mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
if (mo != null)
{
ulong ramBytes = (ulong)mo["TotalPhysicalMemory"];
double ramGB = Math.Round(ramBytes / (1024.0 * 1024.0 * 1024.0));
return $"{ramGB} GB";
}
return "N/A";
}
catch { return "N/A"; }
}
public List<string> GetGPUs()
{
var gpus = new List<string>();
if (!OperatingSystem.IsWindows()) return gpus;
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Name FROM Win32_VideoController");
foreach (ManagementObject mo in searcher.Get())
{
gpus.Add(mo["Name"]?.ToString()?.Trim() ?? "");
}
}
catch { /* Ignore WMI errors */ }
return gpus;
}
public List<StorageDeviceInfo> GetStorage()
{
var drives = new List<StorageDeviceInfo>();
if (!OperatingSystem.IsWindows()) return drives;
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher(@"root\Microsoft\Windows\Storage", "SELECT * FROM MSFT_PhysicalDisk");
foreach (ManagementObject mo in searcher.Get())
{
string mediaType = "Unknown";
switch ((ushort)mo["MediaType"])
{
case 3: mediaType = "HDD"; break;
case 4: mediaType = "SSD"; break;
}
drives.Add(new StorageDeviceInfo
{
Model = mo["Model"]?.ToString()?.Trim(),
SerialNumber = mo["SerialNumber"]?.ToString()?.Trim(),
MediaType = mediaType,
InterfaceType = mo["BusType"]?.ToString()?.Trim(),
Size = (ulong)mo["Size"]
});
}
}
catch { /* Ignore WMI errors */ }
return drives;
}
public List<PrinterInfo> GetPrinters()
{
var printers = new List<PrinterInfo>();
if (!OperatingSystem.IsWindows()) return printers;
var ignoredPrinterNames = new List<string>
{
"Microsoft Print to PDF", "Microsoft XPS Document Writer", "OneNote", "Fax", "Send to OneNote",
"Adobe PDF", "CutePDF", "Bullzip", "doPDF", "Foxit", "PrimoPDF", "PDF Architect",
"Snagit", "WebEx", "AnyDesk"
};
try
{
// PRIMARY METHOD: Use Win32_Printer. This is the most detailed and works when run with user context (e.g., AdminTool).
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Printer");
foreach (ManagementObject mo in searcher.Get())
{
string? name = mo["Name"]?.ToString()?.Trim();
if (string.IsNullOrEmpty(name) || ignoredPrinterNames.Any(p => name.Contains(p, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
// Attempt to get serial number from PNPDeviceID (mostly for USB)
string? pnpDeviceId = mo["PNPDeviceID"]?.ToString();
string? serialNumber = GetSerialFromPnpId(pnpDeviceId);
string? ipAddress = null;
bool isNetwork = (bool)mo["Network"];
string? portName = mo["PortName"]?.ToString()?.Trim();
string? serverName = mo["ServerName"]?.ToString()?.Trim(); // Key for shared printers
// If it's a shared printer from another server, resolve the server's IP.
if (!string.IsNullOrEmpty(serverName))
{
ipAddress = ResolveHostnameToIp(serverName);
}
// Otherwise, if it's a local network printer, get IP from the port name.
else if (isNetwork || (portName?.StartsWith("IP_") ?? false))
{
ipAddress = GetIpFromPortName(portName);
}
if (string.IsNullOrEmpty(serialNumber) && !string.IsNullOrEmpty(ipAddress))
{
if (!string.IsNullOrEmpty(ipAddress))
{
serialNumber = GetPrinterSerialViaSnmp(ipAddress);
}
}
if (!string.IsNullOrEmpty(pnpDeviceId) && pnpDeviceId.Contains("USB"))
{
serialNumber ??= GetSerialFromPnpId(pnpDeviceId);
}
printers.Add(new PrinterInfo
{
Name = name,
DriverName = mo["DriverName"]?.ToString()?.Trim(),
IsShared = (bool)mo["Shared"],
IsNetwork = isNetwork,
HostName = mo["SystemName"]?.ToString()?.Trim(),
PortName = portName,
SerialNumber = serialNumber
});
}
}
catch { /* Ignore WMI errors, proceed to fallback */ }
// FALLBACK METHOD: If Win32_Printer found nothing (common for LocalSystem service), query the registry.
if (printers.Count == 0)
{
try
{
using (RegistryKey? printKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Print\Printers"))
{
if (printKey != null)
{
foreach (string printerName in printKey.GetSubKeyNames())
{
if (ignoredPrinterNames.Any(p => printerName.Contains(p, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
using (RegistryKey? printerSubKey = printKey.OpenSubKey(printerName))
{
if (printerSubKey == null) continue;
string? portName = printerSubKey.GetValue("Port")?.ToString();
bool isNetwork = portName?.StartsWith("IP_") ?? false;
string? serialNumber = null;
// For network printers, try to get the IP from the port name and query SNMP
if (isNetwork)
{
string? ipAddress = GetIpFromPortName(portName);
if (!string.IsNullOrEmpty(ipAddress))
{
serialNumber = GetPrinterSerialViaSnmp(ipAddress);
}
}
var printerInfo = new PrinterInfo
{
Name = printerSubKey.GetValue("Name")?.ToString() ?? printerName,
DriverName = printerSubKey.GetValue("Printer Driver")?.ToString(),
PortName = portName,
IsShared = (printerSubKey.GetValue("Attributes") is int attributes && (attributes & 0x8) != 0), // PRINTER_ATTRIBUTE_SHARED
IsNetwork = isNetwork,
SerialNumber = serialNumber
};
printers.Add(printerInfo);
}
}
}
}
}
catch { /* Silently ignore registry errors */ }
}
return printers.DistinctBy(p => p.Name).ToList();
}
private string? GetSerialFromPnpId(string? pnpDeviceId)
{
if (string.IsNullOrEmpty(pnpDeviceId) || !pnpDeviceId.Contains("USB"))
{
return null;
}
var parts = pnpDeviceId.Split('\\');
// The serial is usually the last part of the ID for USB devices.
// Example: USBPRINT\HEWLETT-PACKARDHP_LASERJET_PROFESSIONAL_M1212NF_MFP\6&1D4A4F6D&0&USB001
// Or: USB\VID_03F0&PID_3B17\CNB2J12345
if (parts.Length > 2 && !parts[2].Contains('&'))
{
return parts[2];
}
return null;
}
private string? GetIpFromPortName(string? portName)
{
if (string.IsNullOrEmpty(portName)) return null;
// Standard TCP/IP Port names are often "IP_192.168.1.100"
if (portName.StartsWith("IP_"))
{
return portName.Substring(3);
}
// WSD ports can also contain an IP address
if (portName.StartsWith("WSD-") && IPAddress.TryParse(portName.Split('/').LastOrDefault(), out _))
{
return portName.Split('/').LastOrDefault();
}
return null;
}
private string? ResolveHostnameToIp(string? hostname)
{
if (string.IsNullOrWhiteSpace(hostname)) return null;
// Clean up hostname if it's a UNC path like \\SERVER
hostname = hostname.TrimStart('\\');
try
{
// GetHostAddresses can return multiple IPs (IPv6, IPv4). Prioritize IPv4.
var addresses = Dns.GetHostAddresses(hostname);
return addresses.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork)?.ToString();
}
catch (SocketException) { /* Host not found */ }
return null;
}
private string? GetPrinterSerialViaSnmp(string ipAddress, string community = "public", int timeout = 2000)
{
// OID for Printer MIB v2 serial number: 1.3.6.1.2.1.43.5.1.1.17.1
byte[] oid = { 0x2b, 0x06, 0x01, 0x02, 0x01, 0x2b, 0x05, 0x01, 0x01, 0x11, 0x01 };
try
{
using var udpClient = new UdpClient();
udpClient.Client.SendTimeout = timeout;
udpClient.Client.ReceiveTimeout = timeout;
// Construct SNMP GetRequest packet
var packet = new List<byte>();
packet.AddRange(new byte[] { 0x30, 0x82, 0x00, 0x00 }); // Sequence, length to be filled
packet.AddRange(new byte[] { 0x02, 0x01, 0x01 }); // Version (SNMPv2c)
packet.AddRange(new byte[] { 0x04, (byte)community.Length }); // Community string
packet.AddRange(Encoding.ASCII.GetBytes(community));
packet.AddRange(new byte[] { 0xA0, 0x82, 0x00, 0x00 }); // GetRequest-PDU, length to be filled
packet.AddRange(new byte[] { 0x02, 0x04, 0x00, 0x00, 0x00, 0x01 }); // Request ID
packet.AddRange(new byte[] { 0x02, 0x01, 0x00, 0x02, 0x01, 0x00 }); // Error status, Error index
packet.AddRange(new byte[] { 0x30, (byte)(oid.Length + 2) }); // Variable bindings
packet.AddRange(new byte[] { 0x30, (byte)oid.Length }); // Varbind
packet.AddRange(new byte[] { 0x06, (byte)oid.Length }); // OID
packet.AddRange(oid);
packet.AddRange(new byte[] { 0x05, 0x00 }); // Null
// Update lengths
// This is a simplified packet construction. A full library would be more robust.
var endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), 161);
udpClient.Send(packet.ToArray(), packet.Count, endpoint);
byte[] received = udpClient.Receive(ref endpoint);
// Basic parsing: find the serial number string in the response.
// A proper ASN.1 parser would be better, but this works for simple string responses.
for (int i = 0; i < received.Length - 1; i++)
{
if (received[i] == 0x04) // Octet String
{
int len = received[i + 1];
if (i + 2 + len <= received.Length && len > 0)
{
return Encoding.ASCII.GetString(received, i + 2, len).Trim();
}
}
}
}
catch { /* Ignore SNMP errors (timeout, host unreachable, etc.) */ }
return null;
}
public bool HasOpticalDrive()
{
if (!OperatingSystem.IsWindows()) return false;
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_CDROMDrive");
return searcher.Get().Count > 0;
}
catch { return false; }
}
public List<MonitorInfo> GetMonitors()
{
var monitors = new List<MonitorInfo>();
var foundPnpDeviceIds = new HashSet<string>();
if (!OperatingSystem.IsWindows()) return monitors;
// PRIMARY METHOD: WMI (root\wmi) - Often has parsed data.
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher(@"root\wmi", "SELECT InstanceName, SerialNumberID, UserFriendlyName FROM WmiMonitorID");
foreach (ManagementObject mo in searcher.Get())
{
string model = ParseMonitorString(mo["UserFriendlyName"]);
string serial = ParseMonitorString(mo["SerialNumberID"]);
string pnpDeviceId = mo["InstanceName"]?.ToString()?.TrimEnd('_', '0').Trim() ?? string.Empty;
if (!string.IsNullOrEmpty(pnpDeviceId))
{
monitors.Add(new MonitorInfo
{
Model = model,
SerialNumber = serial,
PnPDeviceID = pnpDeviceId
});
foundPnpDeviceIds.Add(pnpDeviceId);
}
}
}
catch { /* Ignore WMI errors, proceed to fallback */ }
// FALLBACK METHOD: Registry EDID parsing. This is more reliable for serial numbers.
try
{
using RegistryKey? displayKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\DISPLAY");
if (displayKey != null)
{
foreach (string manufacturerId in displayKey.GetSubKeyNames())
{
using RegistryKey? manufacturerKey = displayKey.OpenSubKey(manufacturerId);
if (manufacturerKey == null) continue;
foreach (string deviceId in manufacturerKey.GetSubKeyNames())
{
string pnpDeviceId = $"DISPLAY\\{manufacturerId}\\{deviceId}";
// If we already found this monitor via WMI, skip it to avoid duplicates.
if (foundPnpDeviceIds.Contains(pnpDeviceId, StringComparer.OrdinalIgnoreCase))
{
continue;
}
using RegistryKey? deviceKey = manufacturerKey.OpenSubKey(deviceId);
if (deviceKey == null) continue;
using RegistryKey? deviceParamsKey = deviceKey.OpenSubKey("Device Parameters");
if (deviceParamsKey != null && deviceParamsKey.GetValue("EDID") is byte[] edid)
{
var (model, serial) = ParseEdid(edid);
monitors.Add(new MonitorInfo
{
Model = model,
SerialNumber = serial,
PnPDeviceID = pnpDeviceId
});
}
}
}
}
}
catch { /* Ignore registry errors */ }
return monitors.Where(m => m.Model != "N/A" || m.SerialNumber != "N/A").DistinctBy(m => m.SerialNumber).ToList();
}
private string ParseMonitorString(object? value)
{
if (value is ushort[] chars)
{
return Encoding.ASCII.GetString(chars.Select(c => (byte)c).ToArray()).Trim('\0', ' ', '\r', '\n');
}
return "N/A";
}
private (string Model, string Serial) ParseEdid(byte[] edid)
{
string model = "N/A";
string serial = "N/A";
// EDID descriptor blocks start at byte 54. There are 4 blocks of 18 bytes each.
for (int i = 54; i < edid.Length && i + 18 <= edid.Length; i += 18)
{
// Check for a descriptor block (bytes 0-2 should not be 00 00 00)
if (edid[i] == 0x00 && edid[i + 1] == 0x00 && edid[i + 2] == 0x00)
{
byte type = edid[i + 3];
if (type == 0xFC) model = Encoding.ASCII.GetString(edid, i + 5, 13).Trim('\r', '\n', '\0', ' '); // Display Name
if (type == 0xFF) serial = Encoding.ASCII.GetString(edid, i + 5, 13).Trim('\r', '\n', '\0', ' '); // Serial Number
}
}
return (model, serial);
}
private string? GetWmiProperty(string wmiClass, string property)
{
try
{
var searcher = new ManagementObjectSearcher($"SELECT {property} FROM {wmiClass}");
var mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
return mo?[property]?.ToString()?.Trim();
}
catch
{
return null;
}
}
private bool IsValidSerialNumber(string? serial)
{
return !string.IsNullOrWhiteSpace(serial) &&
!serial.Equals("To be filled by O.E.M.", StringComparison.OrdinalIgnoreCase) &&
!serial.Equals("Default string", StringComparison.OrdinalIgnoreCase) &&
!serial.Equals("None", StringComparison.OrdinalIgnoreCase);
}
public string? GetOSVersion()
{
if (!OperatingSystem.IsWindows()) return "N/A";
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem");
ManagementObject? mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
return mo?["Caption"]?.ToString()?.Trim();
}
catch { return "N/A"; }
}
public DateTime? GetOSInstallDate()
{
if (!OperatingSystem.IsWindows()) return null;
try
{
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT InstallDate FROM Win32_OperatingSystem");
ManagementObject? mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
var installDate = mo?["InstallDate"]?.ToString();
return installDate != null ? ManagementDateTimeConverter.ToDateTime(installDate) : null;
}
catch { return null; }
}
public string? GetOSLicenseKey()
{
if (!OperatingSystem.IsWindows()) return "N/A";
try
{
// This is a common method but may not work on all systems, especially with newer licensing methods.
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT OA3xOriginalProductKey FROM SoftwareLicensingService");
ManagementObject? mo = searcher.Get().OfType<ManagementObject>().FirstOrDefault();
return mo?["OA3xOriginalProductKey"]?.ToString()?.Trim();
}
catch { return "N/A"; }
}
public List<LocalAdminInfo> GetLocalAdmins()
{
var admins = new List<LocalAdminInfo>();
if (!OperatingSystem.IsWindows()) return admins;
try
{
using (var context = new PrincipalContext(ContextType.Machine))
{
var group = GroupPrincipal.FindByIdentity(context, "Administrators");
if (group != null)
{
// Perform a recursive search to find all members, including those in nested groups.
// Then, use DistinctBy to remove duplicates based on the unique SID.
#pragma warning disable CA1416 // Validate platform compatibility
var uniqueMembers = group.GetMembers(true).DistinctBy(p => p.Sid);
#pragma warning restore CA1416 // Validate platform compatibility
foreach (Principal principal in uniqueMembers)
{
var adminInfo = new LocalAdminInfo
{
Name = principal.Name,
Sid = principal.Sid.ToString(),
AccountType = principal is UserPrincipal ? "User" : "Group",
Source = principal.Context.ContextType == ContextType.Machine ? "Local" : "Domain"
};
if (principal is UserPrincipal user)
{
adminInfo.IsEnabled = user.Enabled;
adminInfo.IsLockedOut = user.IsAccountLockedOut();
}
admins.Add(adminInfo);
}
}
}
}
catch { /* Ignore errors, e.g. on non-domain machines */ }
return admins;
}
public (List<string> IPAddresses, string? MACAddress) GetNetworkInfo()
{
try
{
var allIps = new List<string>();
string? primaryMac = null;
var operationalNics = NetworkInterface.GetAllNetworkInterfaces()
.Where(ni => ni.OperationalStatus == OperationalStatus.Up &&
ni.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
ni.NetworkInterfaceType != NetworkInterfaceType.Tunnel)
.Select(ni => new
{
Interface = ni,
IPProps = ni.GetIPProperties()
})
.Where(x => !(x.Interface.Description.Contains("Virtual") || x.Interface.Name.Contains("Virtual")));
foreach (var nic in operationalNics)
{
allIps.AddRange(nic.IPProps.UnicastAddresses
.Where(ua => ua.Address.AddressFamily == AddressFamily.InterNetwork)
.Select(ua => ua.Address.ToString()));
}
// Find the primary MAC address from the interface with a gateway, ordered by speed
var primaryNic = operationalNics.Where(n => n.IPProps.GatewayAddresses.Any()).OrderByDescending(n => n.Interface.Speed).FirstOrDefault();
primaryMac = primaryNic?.Interface.GetPhysicalAddress().ToString();
return (allIps.Distinct().ToList(), primaryMac);
}
catch { /* Ignore errors */ }
return (new List<string>(), "N/A");
}
}
}