diff --git a/README.md b/README.md index aef40f2..dfc6bb5 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,18 @@ HWID Checker is a Go application that allows you to easily gather various hardwa - Display RAM serial number - Display Windows product ID - Display MAC addresses +- Display TPM status and TPM Endorsement Key +- Display Secure Boot status ### Advanced Features - **Timestamped File Output:** Save all information to uniquely timestamped text files (format: `hwid_info_YYYY-MM-DD_HH-MM-SS.txt`) to prevent overwriting previous scans +- **Clean HWID List:** Generate a compact, human-readable summary (`hwid_clean_YYYY-MM-DD_HH-MM-SS.txt`) with one line per identifier instead of raw command output +- **Scan Comparison:** Compare two previous scan files to see what changed between runs +- **Administrator Detection:** Reports whether the process is elevated, both on-screen and in the file header, since TPM/Secure Boot/Endorsement Key checks require Administrator privileges to return real data - **Real-time Progress Tracking:** Visual progress indicators showing completion percentage and status during full system scans -- **Comprehensive Error Handling:** Robust error handling with detailed logging and graceful recovery from failures +- **Comprehensive Error Handling:** Robust error handling with detailed logging and graceful recovery from failures, including detection of commands that report a privilege error or return an empty result while still exiting successfully - **Multiple Fallback Commands:** Automatic command fallback when primary commands fail, ensuring maximum compatibility +- **Locale-Independent:** Checks use CIM/PowerShell property access instead of parsing localized command-line text, so results are consistent regardless of the system's display language - **PowerShell Integration:** Support for both WMI (legacy) and CIM (modern) PowerShell cmdlets - **Detailed Status Reporting:** Success/failure tracking with execution time and success rate statistics - **Enhanced User Experience:** Clear visual feedback with [Starting], [Complete], [Success], and [Failed] status tags @@ -34,7 +40,7 @@ You can download the pre-compiled executable for Windows from the [Releases](htt - Windows operating system - PowerShell (any version, improved functionality with PowerShell 3.0+) -- Administrator privileges recommended for complete hardware information access +- Administrator privileges required for complete results — TPM Status, TPM Endorsement Key, and Secure Boot all fail or return incomplete data without elevation. The application warns on startup and records `Administrator: Yes/No` in every report if not run elevated. **Note on OS Support:** While HWID Checker is primarily developed and optimized for Windows, the Go programming language allows for cross-platform compilation. However, this tool relies heavily on Windows-specific commands (WMIC, PowerShell, cmd.exe) and will not function correctly on other operating systems without significant modifications. If you're interested in using HWID Checker on another operating system, you would need to modify the source code to use platform-appropriate system commands. @@ -88,8 +94,11 @@ Select an option: 8. RAM (Serial Number) 9. Windows Product ID 10. MAC Addresses -11. Print All to File and Save -12. Exit +11. TPM and Secure Boot Status +12. Print All to File (Detailed) +13. Print Clean HWID List +14. Compare with Previous Scan +15. Exit ======================================== ``` @@ -112,51 +121,72 @@ XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Press Enter to continue... ``` -#### Save All Information to File +#### Save All Information to File (Detailed) ``` -Enter your choice: 11 +Enter your choice: 12 ======================================== Starting full system scan... -Output file: hwid_info_2024-10-14_15-30-45.txt +Output file: hwid_info_2026-08-23_21-08-45.txt ======================================== -[1/14] (0.0%) Processing: SMBIOS (UUID) -[1/14] SUCCESS - SMBIOS (UUID) -[2/14] (7.1%) Processing: BIOS (Serial Number) -[2/14] SUCCESS - BIOS (Serial Number) -[3/14] (14.3%) Processing: Motherboard (Serial Number) -[3/14] SUCCESS - Motherboard (Serial Number) +[1/17] (0.0%) Processing: SMBIOS (UUID) +[1/17] SUCCESS - SMBIOS (UUID) +[2/17] (5.9%) Processing: BIOS (Serial Number) +[2/17] SUCCESS - BIOS (Serial Number) +[3/17] (11.8%) Processing: Motherboard (Serial Number) +[3/17] SUCCESS - Motherboard (Serial Number) ... -[14/14] (92.9%) Processing: MAC Addresses (IPConfig) -[14/14] SUCCESS - MAC Addresses (IPConfig) +[17/17] (94.1%) Processing: Secure Boot +[17/17] SUCCESS - Secure Boot ======================================== Scan Complete ======================================== -Total Commands: 14 -Successful: 14 +Total Commands: 17 +Successful: 17 Failed: 0 Success Rate: 100.0% -Execution Time: 5.234s -Output saved to: hwid_info_2024-10-14_15-30-45.txt +Execution Time: 6.419s +Output saved to: hwid_info_2026-08-23_21-08-45.txt ======================================== Press Enter to continue... ``` +#### Print Clean HWID List +``` +Enter your choice: 13 + +======================================== +Generating clean HWID list... +Output file: hwid_clean_2026-08-23_21-00-53.txt +======================================== +... +``` +Produces a compact summary file, one line per identifier: +``` +SMBIOS (UUID): XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +Motherboard (Serial Number): XXXXXXXXXXXXXXXX +Volume Information: C: 470.88 GB free of 952.98 GB, G: 14.83 GB free of 953.85 GB, E: 205.34 GB free of 447.11 GB +MAC Addresses (GetMac): XX:XX:XX:XX:XX:XX, XX:XX:XX:XX:XX:XX, ... +TPM Status: Activated=True, Enabled=True, Owned=True, SpecVersion=2.0, 0, 1.38 +Secure Boot: True +``` + ### Output File Format -When you select option 11, a timestamped file is created with the following structure: +When you select option 12, a timestamped file is created with the following structure: ``` ======================================== Hardware ID Information Report ======================================== -Generated: 2024-10-14 15:30:45 +Generated: 2026-08-23 21:08:45 System: Windows +Administrator: Yes ======================================== -[1/14] SMBIOS (UUID) +[1/17] SMBIOS (UUID) ======================================== Primary Command: wmic csproduct get uuid Status: SUCCESS @@ -169,12 +199,12 @@ XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX ======================================== Report Summary ======================================== -Total Commands Executed: 14 -Successful: 14 +Total Commands Executed: 17 +Successful: 17 Failed: 0 Success Rate: 100.0% -Execution Time: 5.234s -Completion Time: 2024-10-14 15:30:50 +Execution Time: 6.419s +Completion Time: 2026-08-23 21:08:52 ======================================== ``` @@ -229,11 +259,24 @@ Real-time progress indication includes: - Check available disk space - Verify antivirus isn't blocking file creation +### TPM Status, TPM Endorsement Key, or Secure Boot report FAILED +- These checks require Administrator privileges; the report header's `Administrator:` line confirms whether the process was elevated +- Even when elevated, some hardware/firmware combinations don't expose a TPM Endorsement Key — the report will show the actual PowerShell error message rather than a blank result + ## License This project is licensed under the AGPL-3.0 License. See the [LICENSE](LICENSE) file for more details. ## Changelog +- Added TPM Endorsement Key check as a real per-chip TPM identifier +- Added Administrator-privilege detection, with a startup warning and an `Administrator:` field in every report +- Fixed commands (e.g. `Get-Tpm`, `Confirm-SecureBootUEFI`, `Get-TpmEndorsementKeyInfo`) being logged as SUCCESS when they actually printed a privilege error or returned an empty result +- Fixed a Windows command-line quoting bug that broke `findstr`-based piped commands +- Replaced locale-dependent `findstr` label parsing (English-only `systeminfo`/`ipconfig` labels) with locale-independent CIM/PowerShell equivalents +- Fixed the Clean HWID List producing garbled, unreadable output for MAC address, volume, and TPM checks +- Added TPM and Secure Boot status checks +- Implemented Clean HWID List generation (option 13) +- Added scan comparison functionality (option 14) - Added timestamped file output to prevent overwriting previous scans - Implemented real-time progress tracking with percentage indicators - Improved visual feedback with status tags diff --git a/main.go b/main.go index 21eb327..21f6283 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,21 @@ package main import ( "bufio" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding/asn1" + "encoding/binary" "fmt" + "math/big" "os" "os/exec" "path/filepath" + "regexp" "strings" + "syscall" "time" + "unsafe" ) type Command struct { @@ -26,9 +35,209 @@ type HWIDData struct { value string } +// isRunningAsAdmin reports whether the process has administrator privileges. +// Opening a physical drive handle requires elevation on Windows, so a +// successful open is a reliable signal without extra dependencies. +func isRunningAsAdmin() bool { + f, err := os.Open(`\\.\PHYSICALDRIVE0`) + if err != nil { + return false + } + _ = f.Close() + return true +} + +// isPrivilegeError detects command output that reports missing admin rights +// even though the process itself exited successfully (e.g. Get-Tpm prints a +// localized "requires administrator privileges" message to stdout and still +// returns exit code 0). Without this check that output gets recorded as a +// SUCCESS with garbage content instead of a clear FAILED result. +var emptyTableSeparatorPattern = regexp.MustCompile(`^-+(\s+-+)*$`) + +// isEmptyTableOutput detects a PowerShell formatted table with a header row +// and dashed separator but zero data rows — e.g. Get-TpmEndorsementKeyInfo +// silently returns nothing (no error, exit 0) when not run elevated. That +// text is non-empty, so without this check it slips past as a false SUCCESS. +func isEmptyTableOutput(output string) bool { + var nonEmpty []string + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line != "" { + nonEmpty = append(nonEmpty, line) + } + } + return len(nonEmpty) == 2 && emptyTableSeparatorPattern.MatchString(nonEmpty[1]) +} + +func isPrivilegeError(output string) bool { + lower := strings.ToLower(output) + phrases := []string{ + "se requiere privilegios de administrador", + "acceso denegado", + "privilegios adecuados", + "access is denied", + "access denied", + "administrator privileges are required", + "run as administrator", + "requires elevation", + "you must run this cmdlet from an elevated", + } + for _, phrase := range phrases { + if strings.Contains(lower, phrase) { + return true + } + } + return false +} + +// The following replicates how Samuel Tulach's tpm-info.exe reads the TPM +// Endorsement Key: it bypasses PowerShell/WMI entirely and calls the CNG +// Platform Crypto Provider directly (NCryptOpenStorageProvider on "Microsoft +// Platform Crypto Provider", then NCryptGetProperty for "PCP_EKPUB", which +// returns a raw BCRYPT_RSAPUBLIC_BLOB). That's why Get-TpmEndorsementKeyInfo's +// PublicKeyHash never matches its output — PowerShell hashes its own internal +// byte layout, while tpm-info.exe DER-encodes the raw key as a PKCS#1 +// RSAPublicKey and hashes that instead, then prints MD5/SHA1/SHA256 of it. +var ( + ncryptDLL = syscall.NewLazyDLL("ncrypt.dll") + procNCryptOpenStorageProvider = ncryptDLL.NewProc("NCryptOpenStorageProvider") + procNCryptGetProperty = ncryptDLL.NewProc("NCryptGetProperty") + procNCryptFreeObject = ncryptDLL.NewProc("NCryptFreeObject") +) + +const bcryptRSAPublicMagic = 0x31415352 // "RSA1" little-endian, per bcrypt.h + +func utf16Ptr(s string) *uint16 { + p, err := syscall.UTF16PtrFromString(s) + if err != nil { + return nil + } + return p +} + +// tulachEKHash pulls the raw TPM Endorsement Key public key via the CNG +// Platform Crypto Provider (the same low-level API tpm-info.exe uses), +// DER-encodes it as a PKCS#1 RSAPublicKey, and returns its MD5/SHA1/SHA256 +// hashes in the same labeled format tpm-info.exe prints. +func tulachEKHash() (string, error) { + providerName := utf16Ptr("Microsoft Platform Crypto Provider") + if providerName == nil { + return "", fmt.Errorf("failed to encode provider name") + } + + var hProvider uintptr + ret, _, _ := procNCryptOpenStorageProvider.Call( + uintptr(unsafe.Pointer(&hProvider)), + uintptr(unsafe.Pointer(providerName)), + 0, + ) + if ret != 0 { + return "", fmt.Errorf("NCryptOpenStorageProvider failed: 0x%08X", uint32(ret)) + } + defer procNCryptFreeObject.Call(hProvider) + + propName := utf16Ptr("PCP_EKPUB") + if propName == nil { + return "", fmt.Errorf("failed to encode property name") + } + + var cbResult uint32 + ret, _, _ = procNCryptGetProperty.Call( + hProvider, + uintptr(unsafe.Pointer(propName)), + 0, + 0, + uintptr(unsafe.Pointer(&cbResult)), + 0, + ) + if ret != 0 || cbResult == 0 { + return "", fmt.Errorf("NCryptGetProperty (size query) failed: 0x%08X", uint32(ret)) + } + + buf := make([]byte, cbResult) + ret, _, _ = procNCryptGetProperty.Call( + hProvider, + uintptr(unsafe.Pointer(propName)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(cbResult), + uintptr(unsafe.Pointer(&cbResult)), + 0, + ) + if ret != 0 { + return "", fmt.Errorf("NCryptGetProperty failed: 0x%08X", uint32(ret)) + } + buf = buf[:cbResult] + + // BCRYPT_RSAKEY_BLOB header: Magic, BitLength, cbPublicExp, cbModulus, + // cbPrime1, cbPrime2 (6 x uint32), followed by PublicExponent then Modulus. + if len(buf) < 24 { + return "", fmt.Errorf("EK public key blob too short (%d bytes)", len(buf)) + } + + magic := binary.LittleEndian.Uint32(buf[0:4]) + if magic != bcryptRSAPublicMagic { + return "", fmt.Errorf("unsupported EK key type (magic 0x%08X, expected RSA)", magic) + } + + cbPublicExp := int(binary.LittleEndian.Uint32(buf[8:12])) + cbModulus := int(binary.LittleEndian.Uint32(buf[12:16])) + + offset := 24 + if offset+cbPublicExp+cbModulus > len(buf) { + return "", fmt.Errorf("EK public key blob truncated") + } + + exponent := new(big.Int).SetBytes(buf[offset : offset+cbPublicExp]) + offset += cbPublicExp + modulus := new(big.Int).SetBytes(buf[offset : offset+cbModulus]) + + der, err := asn1.Marshal(struct { + Modulus *big.Int + Exponent *big.Int + }{modulus, exponent}) + if err != nil { + return "", fmt.Errorf("DER encoding failed: %v", err) + } + + md5Sum := md5.Sum(der) + sha1Sum := sha1.Sum(der) + sha256Sum := sha256.Sum256(der) + + return fmt.Sprintf("MD5: %x\nSHA1: %x\nSHA256: %x", md5Sum, sha1Sum, sha256Sum), nil +} + +const nativeTulachEKMarker = "__native_tulach_ek__" + +var nativeChecks = map[string]func() (string, error){ + nativeTulachEKMarker: tulachEKHash, +} + +const tpmEKMethodExplanation = `[Note] Why "TPM Endorsement Key" and "TPM Endorsement Key (Tulach Method)" print different values: + Both read the exact same physical Endorsement Key burned into the TPM chip - + the strings differ because each method hashes a different byte encoding of + that key, not because the underlying key data is different. + + - "TPM Endorsement Key" calls PowerShell's Get-TpmEndorsementKeyInfo, which + returns Microsoft's own internally-computed PublicKeyHash property. This + requires Administrator privileges. + - "TPM Endorsement Key (Tulach Method)" reads the raw EK public key + directly from the CNG Platform Crypto Provider (ncrypt.dll) via + NCryptOpenStorageProvider/NCryptGetProperty("PCP_EKPUB") - the same + low-level API Samuel Tulach's tpm-info.exe uses - then DER-encodes it as + a PKCS#1 RSAPublicKey and hashes that with MD5/SHA1/SHA256. This does + not require elevation, and its output has been verified to match the + real tpm-info.exe tool byte-for-byte. +` + func main() { reader := bufio.NewReader(os.Stdin) + if !isRunningAsAdmin() { + fmt.Println("\n[Warning] Not running as Administrator — TPM, Secure Boot, and some") + fmt.Println(" other checks will fail or return incomplete data.") + fmt.Println(" Re-launch this program as Administrator for full results.") + } + for { fmt.Println("\n========================================") fmt.Println(" HWID Checker") @@ -162,9 +371,9 @@ func main() { }) fmt.Println("\n[Starting] Windows Product ID Check (Alternative)...") runCommandWithFallbacks("Windows Product ID (Alternative)", Command{ - primary: []string{"systeminfo", "|", "findstr", "/B", "/C:\"OS Serial Number\""}, + primary: []string{"powershell", "-Command", "(Get-CimInstance -ClassName Win32_OperatingSystem).SerialNumber"}, fallbacks: [][]string{ - {"powershell", "-Command", "systeminfo | Select-String 'OS Serial Number'"}, + {"powershell", "-Command", "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -Name ProductId | Select-Object -ExpandProperty ProductId"}, }, }) fmt.Println("[Complete] Windows Product ID Check finished") @@ -193,9 +402,9 @@ func main() { }) fmt.Println("\n[Starting] MAC Addresses Check (4/4)...") runCommandWithFallbacks("MAC Addresses (IPConfig)", Command{ - primary: []string{"ipconfig", "/all", "|", "findstr", `"Physical Address"`}, + primary: []string{"powershell", "-Command", "Get-NetAdapter | Select-Object Name, MacAddress, Status"}, fallbacks: [][]string{ - {"powershell", "-Command", "ipconfig /all | Select-String 'Physical Address'"}, + {"powershell", "-Command", "Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.MACAddress -ne $null } | Select-Object Description, MACAddress"}, }, }) fmt.Println("[Complete] MAC Addresses Check finished") @@ -208,6 +417,17 @@ func main() { {"powershell", "-Command", "Get-CimInstance -Namespace ROOT\\CIMV2\\Security\\MicrosoftTpm -ClassName Win32_Tpm"}, }, }) + fmt.Println("\n[Checking] TPM Endorsement Key...") + runCommandWithFallbacks("TPM Endorsement Key", Command{ + primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo"}, + fallbacks: [][]string{}, + }) + fmt.Println("\n[Checking] TPM Endorsement Key (Tulach Method)...") + runCommandWithFallbacks("TPM Endorsement Key (Tulach Method)", Command{ + primary: []string{nativeTulachEKMarker}, + fallbacks: [][]string{}, + }) + fmt.Println(tpmEKMethodExplanation) fmt.Println("\n[Checking] Secure Boot Status...") runCommandWithFallbacks("Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, @@ -296,6 +516,14 @@ func executeCommandWithResult(args []string) CommandResult { fmt.Printf("Command: %s\n", strings.Join(args, " ")) + if fn, ok := nativeChecks[args[0]]; ok { + output, err := fn() + if err != nil { + return CommandResult{success: false, error: err.Error()} + } + return CommandResult{success: true, output: output} + } + if containsPipe(args) { return executePipedCommandWithResult(args) } @@ -333,6 +561,22 @@ func executeCommandWithResult(args []string) CommandResult { } } + if isPrivilegeError(outputStr) { + return CommandResult{ + success: false, + error: "Administrator privileges required", + output: outputStr, + } + } + + if isEmptyTableOutput(outputStr) { + return CommandResult{ + success: false, + error: "Command returned no data (possibly requires administrator privileges)", + output: outputStr, + } + } + return CommandResult{ success: true, output: outputStr, @@ -348,7 +592,13 @@ func executePipedCommandWithResult(args []string) CommandResult { } fullCommand := strings.Join(args, " ") - cmd := exec.Command("cmd.exe", "/C", fullCommand) + cmd := exec.Command("cmd.exe") + // fullCommand can contain embedded double quotes (e.g. findstr /C:"OS Serial Number"). + // exec.Command's default Windows argument escaping re-escapes those quotes for CRT-style + // parsing, but cmd.exe parses its command line differently, which splits the quoted + // phrase into separate tokens. Setting CmdLine directly bypasses that re-escaping and + // hands cmd.exe the literal command line it expects. + cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: "cmd.exe /C " + fullCommand} cmd.Env = os.Environ() output, err := cmd.CombinedOutput() @@ -373,6 +623,22 @@ func executePipedCommandWithResult(args []string) CommandResult { } } + if isPrivilegeError(outputStr) { + return CommandResult{ + success: false, + error: "Administrator privileges required", + output: outputStr, + } + } + + if isEmptyTableOutput(outputStr) { + return CommandResult{ + success: false, + error: "Command returned no data (possibly requires administrator privileges)", + output: outputStr, + } + } + return CommandResult{ success: true, output: outputStr, @@ -467,6 +733,11 @@ func saveAllToFile(cleanList bool) { } } else { success = processCommandForFile(file, cmdEntry, progress) + if cmdEntry.description == "TPM Endorsement Key (Tulach Method)" { + if _, err := fmt.Fprintln(file, tpmEKMethodExplanation); err != nil { + logError(fmt.Sprintf("[Warning] Failed to write TPM EK explanation: %s", err)) + } + } } if success { @@ -503,7 +774,7 @@ func processCommandForCleanList(cmdEntry FileCommandEntry) (bool, string) { result := executeCommandWithResult(cmdEntry.command.primary) if result.success { - return true, extractCleanValue(result.output) + return true, extractCleanValue(cmdEntry.description, result.output) } for _, fallback := range cmdEntry.command.fallbacks { @@ -513,14 +784,113 @@ func processCommandForCleanList(cmdEntry FileCommandEntry) (bool, string) { result = executeCommandWithResult(fallback) if result.success { - return true, extractCleanValue(result.output) + return true, extractCleanValue(cmdEntry.description, result.output) } } return false, "" } -func extractCleanValue(output string) string { +// extractCleanValue turns raw command output into a single readable line for +// the clean HWID list. Table-shaped output (MAC adapters, volumes, TPM +// properties) needs dedicated parsing — the old generic line filter just +// stripped known header words and glued every remaining row together, +// producing garbled multi-column dumps for anything wider than one value. +func extractCleanValue(description, output string) string { + lower := strings.ToLower(description) + switch { + case strings.Contains(lower, "mac address"): + return extractMACAddresses(output) + case strings.Contains(lower, "tpm status"), strings.Contains(lower, "tpm endorsement key"): + return extractTPMSummary(output) + case strings.Contains(lower, "volume information"): + return extractVolumeSummary(output) + default: + return extractGenericValue(output) + } +} + +var macAddressPattern = regexp.MustCompile(`(?i)\b([0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b`) + +func extractMACAddresses(output string) string { + matches := macAddressPattern.FindAllString(output, -1) + + seen := make(map[string]bool) + var macs []string + for _, mac := range matches { + normalized := strings.ToUpper(strings.ReplaceAll(mac, "-", ":")) + if normalized == "00:00:00:00:00:00" || seen[normalized] { + continue + } + seen[normalized] = true + macs = append(macs, normalized) + } + + if len(macs) == 0 { + return "Not Available" + } + + return strings.Join(macs, ", ") +} + +var tpmPropertyPattern = regexp.MustCompile(`^(\w+)\s*:\s*(.+)$`) + +func extractTPMSummary(output string) string { + labels := map[string]string{ + "IsActivated_InitialValue": "Activated", + "IsEnabled_InitialValue": "Enabled", + "IsOwned_InitialValue": "Owned", + "SpecVersion": "SpecVersion", + "TpmPresent": "Present", + "TpmReady": "Ready", + "ManufacturerVersion": "FirmwareVersion", + "PhysicalPresenceVersionInfo": "PPIVersion", + "PublicKeyHash": "EKPublicKeyHash", + "ManufacturerId": "ManufacturerId", + } + + var parts []string + for _, line := range strings.Split(output, "\n") { + match := tpmPropertyPattern.FindStringSubmatch(strings.TrimSpace(line)) + if match == nil { + continue + } + key := strings.TrimSpace(match[1]) + label, ok := labels[key] + if !ok { + continue + } + parts = append(parts, fmt.Sprintf("%s=%s", label, strings.TrimSpace(match[2]))) + } + + if len(parts) == 0 { + return extractGenericValue(output) + } + + return strings.Join(parts, ", ") +} + +var volumeRowPattern = regexp.MustCompile(`^([A-Z])\s{2,}\S.*?(\d[\d.]*\s*(?:KB|MB|GB|TB))\s+(\d[\d.]*\s*(?:KB|MB|GB|TB))\s*$`) + +func extractVolumeSummary(output string) string { + var drives []string + for _, line := range strings.Split(output, "\n") { + match := volumeRowPattern.FindStringSubmatch(strings.TrimRight(line, " \t\r")) + if match == nil { + continue + } + letter, free, total := match[1], match[2], match[3] + drives = append(drives, fmt.Sprintf("%s: %s free of %s", letter, free, total)) + } + + if len(drives) == 0 { + return "Not Available" + } + + return strings.Join(drives, ", ") +} + +func extractGenericValue(output string) string { lines := strings.Split(output, "\n") var values []string @@ -852,6 +1222,11 @@ func writeFileHeader(file *os.File, cleanList bool) error { return fmt.Errorf("file is nil") } + adminStatus := "No (run as Administrator for TPM/Secure Boot/full results)" + if isRunningAsAdmin() { + adminStatus = "Yes" + } + var header string if cleanList { header = fmt.Sprintf( @@ -860,8 +1235,10 @@ func writeFileHeader(file *os.File, cleanList bool) error { "========================================\n"+ "Generated: %s\n"+ "System: Windows\n"+ + "Administrator: %s\n"+ "========================================\n\n", time.Now().Format("2006-01-02 15:04:05"), + adminStatus, ) } else { header = fmt.Sprintf( @@ -870,8 +1247,10 @@ func writeFileHeader(file *os.File, cleanList bool) error { "========================================\n"+ "Generated: %s\n"+ "System: Windows\n"+ + "Administrator: %s\n"+ "========================================\n\n", time.Now().Format("2006-01-02 15:04:05"), + adminStatus, ) } @@ -993,9 +1372,9 @@ func buildCommandList() []FileCommandEntry { }, }}, {"Windows Product ID (Alternative)", Command{ - primary: []string{"systeminfo", "|", "findstr", "/B", "/C:\"OS Serial Number\""}, + primary: []string{"powershell", "-Command", "(Get-CimInstance -ClassName Win32_OperatingSystem).SerialNumber"}, fallbacks: [][]string{ - {"powershell", "-Command", "systeminfo | Select-String 'OS Serial Number'"}, + {"powershell", "-Command", "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion' -Name ProductId | Select-Object -ExpandProperty ProductId"}, }, }}, {"MAC Addresses (GetMac)", Command{ @@ -1018,9 +1397,9 @@ func buildCommandList() []FileCommandEntry { }, }}, {"MAC Addresses (IPConfig)", Command{ - primary: []string{"ipconfig", "/all", "|", "findstr", `"Physical Address"`}, + primary: []string{"powershell", "-Command", "Get-NetAdapter | Select-Object Name, MacAddress, Status"}, fallbacks: [][]string{ - {"powershell", "-Command", "ipconfig /all | Select-String 'Physical Address'"}, + {"powershell", "-Command", "Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration | Where-Object { $_.MACAddress -ne $null } | Select-Object Description, MACAddress"}, }, }}, {"TPM Status", Command{ @@ -1030,6 +1409,14 @@ func buildCommandList() []FileCommandEntry { {"powershell", "-Command", "Get-CimInstance -Namespace ROOT\\CIMV2\\Security\\MicrosoftTpm -ClassName Win32_Tpm"}, }, }}, + {"TPM Endorsement Key", Command{ + primary: []string{"powershell", "-Command", "Get-TpmEndorsementKeyInfo"}, + fallbacks: [][]string{}, + }}, + {"TPM Endorsement Key (Tulach Method)", Command{ + primary: []string{nativeTulachEKMarker}, + fallbacks: [][]string{}, + }}, {"Secure Boot", Command{ primary: []string{"powershell", "-Command", "Confirm-SecureBootUEFI"}, fallbacks: [][]string{