#requires -Version 5.1 # TEMPX PC Check - generated from the reviewed repository sources. # Run only with the computer owner's informed consent. & { <# .SYNOPSIS Collects local technical metadata for an authorized customer PC review. .DESCRIPTION Reports tool-name matches with their source and collection status. Does not read file contents, contact a server, or determine identity, employment, or sharing. Obtain the computer owner's informed consent before running this script. #> [CmdletBinding()] param( [ValidateRange(1, 3650)][int]$Days = 30, [ValidateRange(0, 5)][int]$ScanDepth = 2, [string[]]$ScanRoot, [ValidateRange(1, 1000000)][int]$MaxEntriesPerRoot = 20000, [ValidateRange(1, 1000)][int]$MaxRows = 75, [string[]]$LoaderFileName, [switch]$IncludeInventory, [switch]$NoClipboard, [switch]$PassThru ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version 2.0 function Get-TempxRules { # Avoid broad substrings such as "ida" inside "NVIDIA". $definitions = @( @('IDA', 'Disassembler', '(?i)(?&1 | ForEach-Object { if ($_ -is [System.Management.Automation.ErrorRecord]) { $errors.Add($_.Exception.Message) } elseif ($null -ne $_) { $rows.Add($_) } } } catch { $errors.Add($_.Exception.Message) } $timer.Stop() $status = if ($errors.Count -eq 0) { 'Complete' } elseif ($rows.Count -gt 0) { 'Partial' } else { 'Failed' } [pscustomobject]@{ Name = $Name; Status = $status; Rows = $rows.ToArray() Errors = @($errors | Select-Object -Unique); DurationMs = $timer.ElapsedMilliseconds } } function Get-TempxFiles { [CmdletBinding()] param( [AllowEmptyCollection()][string[]]$Roots, [ValidateRange(0, 5)][int]$Depth = 2, [ValidateRange(1, 1000000)][int]$MaxEntriesPerRoot = 20000 ) $extensions = @('.exe', '.dll', '.sys', '.ps1', '.bat', '.cmd', '.msi', '.jar', '.idb', '.i64', '.gpr', '.gzf') $seenRoots = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) $emitted = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($root in $Roots) { $queue = New-Object 'System.Collections.Generic.Queue[object]' try { $rootItem = Get-Item -LiteralPath $root -Force -ErrorAction Stop if (-not $rootItem.PSIsContainer) { throw "Scan root is not a directory: $root" } if ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Skipped reparse point scan root: $root" } if (-not $seenRoots.Add($rootItem.FullName)) { continue } $queue.Enqueue([pscustomobject]@{ Path = $rootItem.FullName; Level = 0 }) } catch { Write-Error -Message $_.Exception.Message -ErrorAction Continue; continue } $count = 0 $capped = $false while ($queue.Count -gt 0 -and -not $capped) { $current = $queue.Dequeue() Get-ChildItem -LiteralPath $current.Path -Force -ErrorAction Continue | Select-Object -First ($MaxEntriesPerRoot - $count + 1) | ForEach-Object { if ($capped) { return } $count++ if ($count -gt $MaxEntriesPerRoot) { $capped = $true; return } if ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) { return } if (($_.PSIsContainer -or $_.Extension -in $extensions) -and $emitted.Add($_.FullName)) { [pscustomobject]@{ Name = $_.Name; FullName = $_.FullName; LastWriteTime = $_.LastWriteTime Kind = $(if ($_.PSIsContainer) { 'Directory' } else { 'File' }) } } if ($_.PSIsContainer -and $current.Level -lt $Depth) { $queue.Enqueue([pscustomobject]@{ Path = $_.FullName; Level = $current.Level + 1 }) } } } if ($capped) { Write-Error "Scan entry limit ($MaxEntriesPerRoot) reached for $root; coverage is incomplete." -ErrorAction Continue } } } function Get-TempxInstalledApplications { foreach ($key in @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' )) { if (-not (Test-Path -LiteralPath $key -ErrorAction Continue)) { continue } Get-ChildItem -LiteralPath $key -ErrorAction Continue | ForEach-Object { $app = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction Continue if (Get-TempxProperty $app 'DisplayName') { $app | Select-Object DisplayName, DisplayVersion, Publisher, InstallLocation } } } } function Get-TempxRecentArtifacts { [CmdletBinding()] param([string]$Path, [string]$Filter = '*', [datetime]$Since) Get-ChildItem -LiteralPath $Path -File -Filter $Filter -Force -ErrorAction Continue | Where-Object LastWriteTime -GE $Since | Sort-Object LastWriteTime -Descending | Select-Object Name, FullName, LastWriteTime } function Get-TempxLogClearEvents { [CmdletBinding()] param([datetime]$Since) foreach ($spec in @(@{ Log = 'System'; ID = 104 }, @{ Log = 'Security'; ID = 1102 })) { try { $null = Get-WinEvent -ListLog $spec.Log -ErrorAction Stop Get-WinEvent -FilterHashtable @{ LogName = $spec.Log; ID = $spec.ID; StartTime = $Since } -ErrorAction Stop | Select-Object @{ N = 'Log'; E = { $spec.Log } }, TimeCreated, Id } catch { if ($_.FullyQualifiedErrorId -notlike 'NoMatchingEventsFound*') { Write-Error -Message $_.Exception.Message -ErrorAction Continue } } } } function Get-TempxMachineIdentity { # Read-only hardware/OS identifiers used to tie one license to one machine. # A differing fingerprint for the same license is a sharing lead, not proof. $safeGet = { param([scriptblock]$Action) try { & $Action } catch { $null } } $cs = & $safeGet { Get-CimInstance Win32_ComputerSystem -ErrorAction Stop } $csp = & $safeGet { Get-CimInstance Win32_ComputerSystemProduct -ErrorAction Stop } $bios = & $safeGet { Get-CimInstance Win32_BIOS -ErrorAction Stop } $base = & $safeGet { Get-CimInstance Win32_BaseBoard -ErrorAction Stop } $cpu = & $safeGet { Get-CimInstance Win32_Processor -ErrorAction Stop | Select-Object -First 1 } $guid = & $safeGet { (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid } $productId = & $safeGet { (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductId -ErrorAction Stop).ProductId } $disks = @(& $safeGet { Get-CimInstance Win32_DiskDrive -ErrorAction Stop } | ForEach-Object { ([string](Get-TempxProperty $_ 'SerialNumber')).Trim() } | Where-Object { $_ }) $macs = @(& $safeGet { Get-CimInstance Win32_NetworkAdapter -ErrorAction Stop } | Where-Object { (Get-TempxProperty $_ 'PhysicalAdapter') -and (Get-TempxProperty $_ 'MACAddress') } | ForEach-Object { [string]$_.MACAddress } | Select-Object -Unique) $fields = [ordered]@{ 'Computer name' = [string]$env:COMPUTERNAME 'Current user' = "$env:USERDOMAIN\$env:USERNAME" 'System UUID' = [string](Get-TempxProperty $csp 'UUID') 'Machine GUID' = [string]$guid 'Windows product ID' = [string]$productId 'Manufacturer' = [string](Get-TempxProperty $cs 'Manufacturer') 'Model' = [string](Get-TempxProperty $cs 'Model') 'BIOS serial' = ([string](Get-TempxProperty $bios 'SerialNumber')).Trim() 'Baseboard serial' = ([string](Get-TempxProperty $base 'SerialNumber')).Trim() 'CPU ID' = [string](Get-TempxProperty $cpu 'ProcessorId') 'Disk serials' = ($disks -join '; ') 'MAC addresses' = ($macs -join '; ') } $seed = (@($fields['System UUID'], $fields['Machine GUID'], $fields['BIOS serial'], $fields['Baseboard serial'], $fields['CPU ID'], $fields['Disk serials']) | Where-Object { $_ }) -join '|' $fingerprint = 'unavailable (insufficient hardware identifiers)' if ($seed) { $sha = [Security.Cryptography.SHA256]::Create() try { $hashBytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($seed)) $fingerprint = -join (@($hashBytes)[0..7] | ForEach-Object { $_.ToString('x2') }) } finally { $sha.Dispose() } } [pscustomobject]@{ Attribute = 'Hardware fingerprint (SHA-256/64-bit)'; Value = $fingerprint } foreach ($key in $fields.Keys) { [pscustomobject]@{ Attribute = $key; Value = [string]$fields[$key] } } } function Invoke-TempxCheck { [CmdletBinding()] param( [ValidateRange(1, 3650)][int]$Days = 30, [ValidateRange(0, 5)][int]$ScanDepth = 2, [string[]]$ScanRoot, [ValidateRange(1, 1000000)][int]$MaxEntriesPerRoot = 20000, [string[]]$LoaderFileName = @(), [switch]$IncludeInventory ) if ($env:OS -ne 'Windows_NT') { throw 'TEMPX PC Check requires Windows.' } foreach ($loader in $LoaderFileName) { if ([string]::IsNullOrWhiteSpace($loader) -or $loader.IndexOfAny([IO.Path]::GetInvalidFileNameChars()) -ge 0 -or $loader -in @('.', '..')) { throw 'LoaderFileName must contain literal filenames, without directory paths or wildcards.' } } $since = (Get-Date).AddDays(-$Days) if (-not $PSBoundParameters.ContainsKey('ScanRoot')) { $ScanRoot = @( [Environment]::GetFolderPath('Desktop'), (Join-Path $env:USERPROFILE 'Downloads'), $env:ProgramFiles, ${env:ProgramFiles(x86)}, (Join-Path $env:LOCALAPPDATA 'Programs') ) | Where-Object { $_ } } $ScanRoot = @($ScanRoot | Select-Object -Unique) $sources = New-Object 'System.Collections.Generic.List[object]' $sources.Add((Invoke-TempxSource 'WINDOWS' { Get-CimInstance Win32_OperatingSystem -ErrorAction Stop | Select-Object Caption, BuildNumber, InstallDate, LastBootUpTime })) $sources.Add((Invoke-TempxSource 'MACHINE IDENTITY' { Get-TempxMachineIdentity })) $sources.Add((Invoke-TempxSource 'INSTALLED APPLICATIONS' { Get-TempxInstalledApplications })) $sources.Add((Invoke-TempxSource 'PROCESSES' { Get-CimInstance Win32_Process -ErrorAction Stop | Select-Object Name, ProcessId, ExecutablePath })) $sources.Add((Invoke-TempxSource 'SERVICES' { Get-CimInstance Win32_Service -ErrorAction Stop | Select-Object Name, DisplayName, State, PathName })) $sources.Add((Invoke-TempxSource 'DRIVERS' { Get-CimInstance Win32_SystemDriver -ErrorAction Stop | Select-Object Name, DisplayName, State, PathName })) $sources.Add((Invoke-TempxSource 'LOCAL FILES' { Get-TempxFiles -Roots $ScanRoot -Depth $ScanDepth -MaxEntriesPerRoot $MaxEntriesPerRoot })) $sources.Add((Invoke-TempxSource 'PREFETCH' { Get-TempxRecentArtifacts -Path (Join-Path $env:WINDIR 'Prefetch') -Filter '*.pf' -Since $since })) $sources.Add((Invoke-TempxSource 'RECENT SHORTCUTS' { Get-TempxRecentArtifacts -Path (Join-Path $env:APPDATA 'Microsoft\Windows\Recent') -Filter '*.lnk' -Since $since })) $sources.Add((Invoke-TempxSource 'STARTUP' { Get-CimInstance Win32_StartupCommand -ErrorAction Stop | Select-Object Name, Command })) $sources.Add((Invoke-TempxSource 'SCHEDULED TASKS' { Get-ScheduledTask -ErrorAction Stop | Where-Object TaskPath -NotLike '\Microsoft\*' | ForEach-Object { $task = $_ foreach ($action in @($task.Actions)) { [pscustomobject]@{ Name = $task.TaskName; TaskPath = $task.TaskPath; State = $task.State Execute = Get-TempxProperty $action 'Execute' Arguments = Get-TempxProperty $action 'Arguments' } } } })) $sources.Add((Invoke-TempxSource 'DEFENDER STATUS' { Get-MpComputerStatus -ErrorAction Stop | Select-Object AntivirusEnabled, RealTimeProtectionEnabled, AMRunningMode })) $sources.Add((Invoke-TempxSource 'DEFENDER DETECTIONS' { Get-MpThreatDetection -ErrorAction Stop | Where-Object InitialDetectionTime -GE $since | Sort-Object InitialDetectionTime -Descending | Select-Object InitialDetectionTime, ThreatID, ActionSuccess })) $sources.Add((Invoke-TempxSource 'DEFENDER THREAT CATALOG' { Get-MpThreat -ErrorAction Stop | Select-Object ThreatID, ThreatName })) $threatNames = @{} foreach ($catalogSource in $sources | Where-Object Name -EQ 'DEFENDER THREAT CATALOG') { foreach ($threat in $catalogSource.Rows) { $threatNames[[string]$threat.ThreatID] = $threat.ThreatName } } foreach ($detectionSource in $sources | Where-Object Name -EQ 'DEFENDER DETECTIONS') { $detectionSource.Rows = @($detectionSource.Rows | ForEach-Object { $name = $threatNames[[string]$_.ThreatID] if (-not $name) { $name = 'Name unavailable; see catalog coverage.' } [pscustomobject]@{ InitialDetectionTime = $_.InitialDetectionTime; ThreatID = $_.ThreatID ThreatName = $name; ActionSuccess = $_.ActionSuccess } }) } $sources.Add((Invoke-TempxSource 'LOG CLEAR EVENTS' { Get-TempxLogClearEvents -Since $since })) if ($IncludeInventory) { $sources.Add((Invoke-TempxSource 'RECYCLE BIN' { $shell = $null; $folder = $null; $items = $null try { $shell = New-Object -ComObject Shell.Application $folder = $shell.Namespace(10) if ($null -eq $folder) { throw 'Recycle Bin not accessible.' } $items = $folder.Items() foreach ($entry in $items) { try { [pscustomobject]@{ Name = $entry.Name; Type = $entry.Type } } finally { if ([Runtime.InteropServices.Marshal]::IsComObject($entry)) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($entry) } } } } finally { foreach ($com in @($items, $folder, $shell)) { if ($null -ne $com -and [Runtime.InteropServices.Marshal]::IsComObject($com)) { $null = [Runtime.InteropServices.Marshal]::ReleaseComObject($com) } } } })) $sources.Add((Invoke-TempxSource 'BROWSER PROFILES' { $paths = @{ Chrome = "$env:LOCALAPPDATA\Google\Chrome\User Data" Edge = "$env:LOCALAPPDATA\Microsoft\Edge\User Data" Brave = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data" Firefox = "$env:APPDATA\Mozilla\Firefox\Profiles" } foreach ($browser in $paths.GetEnumerator() | Sort-Object Key) { if (-not (Test-Path -LiteralPath $browser.Value -ErrorAction Continue)) { continue } Get-ChildItem -LiteralPath $browser.Value -Directory -ErrorAction Continue | Where-Object { $browser.Key -eq 'Firefox' -or $_.Name -eq 'Default' -or $_.Name -match '^Profile \d+$' } | ForEach-Object { [pscustomobject]@{ Browser = $browser.Key; Profile = $_.Name; Modified = $_.LastWriteTime } } } })) $sources.Add((Invoke-TempxSource 'CONNECTED USB DEVICES' { Get-PnpDevice -PresentOnly -ErrorAction Stop | Where-Object InstanceId -Like 'USB\*' | Select-Object FriendlyName, Class, Status })) } $fieldMap = @{ 'INSTALLED APPLICATIONS' = @('DisplayName', 'InstallLocation') 'PROCESSES' = @('Name', 'ExecutablePath') 'SERVICES' = @('Name', 'DisplayName', 'PathName') 'DRIVERS' = @('Name', 'DisplayName', 'PathName') 'LOCAL FILES' = @('Name', 'FullName') 'PREFETCH' = @('Name') 'RECENT SHORTCUTS' = @('Name') 'STARTUP' = @('Name', 'Command') 'SCHEDULED TASKS' = @('Name', 'Execute', 'Arguments') } $rules = @(Get-TempxRules) $findings = @(foreach ($source in $sources) { if ($fieldMap.ContainsKey($source.Name)) { Find-TempxMatch -Rows $source.Rows -Source $source.Name -Fields $fieldMap[$source.Name] -Rules $rules -LoaderFileName $LoaderFileName } }) $coverage = @($sources | ForEach-Object { $sourceName = $_.Name $sourceFindings = @($findings | Where-Object Source -EQ $sourceName) [pscustomobject]@{ Source = $_.Name; Status = $_.Status; Entries = $_.Rows.Count Matches = $sourceFindings.Count; DurationMs = $_.DurationMs; Errors = $_.Errors } }) $contextNames = @('WINDOWS', 'MACHINE IDENTITY', 'DEFENDER STATUS', 'DEFENDER DETECTIONS', 'LOG CLEAR EVENTS') $context = @($sources | Where-Object { $_.Name -in $contextNames } | Select-Object Name, Status, Rows, Errors) $inventory = @() if ($IncludeInventory) { $inventory = @($sources | Where-Object { $_.Name -notin $contextNames -and $_.Name -ne 'DEFENDER THREAT CATALOG' } | ForEach-Object { $sourceRows = $_.Rows if ($_.Name -eq 'LOCAL FILES') { $sourceRows = @($sourceRows | Where-Object LastWriteTime -GE $since) } [pscustomobject]@{ Name = $_.Name; Status = $_.Status; Rows = $sourceRows; Errors = $_.Errors } }) } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() try { $principal = New-Object Security.Principal.WindowsPrincipal($identity) $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } finally { $identity.Dispose() } $fingerprint = 'unavailable' foreach ($identitySource in $sources | Where-Object Name -EQ 'MACHINE IDENTITY') { foreach ($attributeRow in $identitySource.Rows) { if ((Get-TempxProperty $attributeRow 'Attribute') -like 'Hardware fingerprint*') { $fingerprint = [string](Get-TempxProperty $attributeRow 'Value') } } } [pscustomobject]@{ Version = '2.1'; GeneratedAt = (Get-Date).ToString('o'); Fingerprint = $fingerprint Scope = [pscustomobject]@{ Days = $Days; ScanDepth = $ScanDepth; ScanRoots = $ScanRoot MaxEntriesPerRoot = $MaxEntriesPerRoot; Elevated = $isAdmin IncludeInventory = [bool]$IncludeInventory; LoaderFileName = @($LoaderFileName) } Findings = $findings; Coverage = $coverage; Context = $context; Inventory = $inventory } } function ConvertTo-TempxSafeText { param([AllowEmptyString()][string]$Value) [regex]::Replace($Value, '[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]', ' ') } function Format-TempxReport { [CmdletBinding()] param([Parameter(Mandatory)][object]$Report, [ValidateRange(1, 1000)][int]$MaxRows = 75) $lines = New-Object 'System.Collections.Generic.List[string]' $lines.Add('TEMPX PC CHECK v' + $Report.Version + ' - TECHNICAL REVIEW') $lines.Add('Generated: ' + $Report.GeneratedAt) $lines.Add('Name matches require manual review; no identity, employment, or sharing determination.') $lines.Add('No file contents, browser history, or messages read; no credential extraction.') $lines.Add('') $lines.Add('========== QUICK SUMMARY ==========') $lines.Add('Findings: ' + @($Report.Findings).Count + ' (multiple sources may refer to the same tool)') foreach ($toolGroup in $Report.Findings | Group-Object Tool | Sort-Object Name) { $toolSources = @($toolGroup.Group | Select-Object -ExpandProperty Source -Unique) $lines.Add(' ' + $toolGroup.Name + ': ' + $toolGroup.Count + ' match(es); sources: ' + ($toolSources -join ', ')) } $lines.Add('Incomplete sources: ' + @($Report.Coverage | Where-Object Status -NE 'Complete').Count) $lines.Add('Elevated: ' + $Report.Scope.Elevated) $lines.Add('Hardware fingerprint: ' + (Get-TempxProperty $Report 'Fingerprint') + ' (same license on different hardware is a sharing lead, not proof)') $lines.Add('Historical window: ' + $Report.Scope.Days + ' days. Tool files/folders are checked regardless of age.') $lines.Add('Scan depth: ' + $Report.Scope.ScanDepth + '; entry limit per root: ' + $Report.Scope.MaxEntriesPerRoot) $lines.Add('Scan roots: ' + (ConvertTo-TempxSafeText ($Report.Scope.ScanRoots -join '; '))) $lines.Add('Scope: current Windows user registry and profile, machine installs; no other-user profile scan.') $lines.Add('Some process paths can be unavailable even when the process query succeeds.') $lines.Add('Metadata, especially startup/task commands, may contain personal or sensitive information; review before sharing.') $lines.Add('Junctions/symlinks skipped. Renamed, deleted, inaccessible, and out-of-scope tools may be missed.') $lines.Add('A Complete source finished within this scope; it does not establish a clean computer.') $lines.Add('') $lines.Add('========== COLLECTION COVERAGE ==========') foreach ($source in $Report.Coverage) { $lines.Add("$($source.Source): $($source.Status); entries=$($source.Entries); matches=$($source.Matches)") foreach ($errorMessage in $source.Errors) { $lines.Add(' Issue: ' + (ConvertTo-TempxSafeText $errorMessage)) } } $lines.Add('') $lines.Add('========== TOOL AND LOADER FINDINGS ==========') if (@($Report.Findings).Count -eq 0) { $lines.Add('No name matches in collected data. Check coverage above.') } foreach ($group in $Report.Findings | Group-Object Source) { $lines.Add("$($group.Name): $($group.Count) finding(s); showing up to $MaxRows") foreach ($finding in $group.Group | Select-Object -First $MaxRows) { $lines.Add(' ' + $finding.Tool + ' [' + $finding.Category + ']') $lines.Add(' ' + $finding.MatchedField + ': ' + (ConvertTo-TempxSafeText $finding.MatchedValue)) if ($finding.Location) { $lines.Add(' Location: ' + (ConvertTo-TempxSafeText $finding.Location)) } if ($finding.Modified) { $lines.Add(' File modified: ' + $finding.Modified + ' (not an execution timestamp)') } $lines.Add(' ' + $finding.Meaning) } } $lines.Add('') $lines.Add('========== SYSTEM CONTEXT ==========') foreach ($section in @($Report.Context) + @($Report.Inventory)) { $lines.Add("$($section.Name): $($section.Status); entries=$(@($section.Rows).Count); showing up to $MaxRows") if (@($section.Rows).Count -gt 0) { $safeRows = @($section.Rows | Select-Object -First $MaxRows | ForEach-Object { $safe = [ordered]@{} foreach ($property in $_.PSObject.Properties) { $safe[$property.Name] = ConvertTo-TempxSafeText ([string]$property.Value) } [pscustomobject]$safe }) $table = $safeRows | Format-Table -AutoSize -Wrap | Out-String -Width 200 $lines.Add($table.TrimEnd()) } } $lines -join [Environment]::NewLine } $checkOptions = @{ Days = $Days ScanDepth = $ScanDepth MaxEntriesPerRoot = $MaxEntriesPerRoot LoaderFileName = $LoaderFileName IncludeInventory = [bool]$IncludeInventory } if ($PSBoundParameters.ContainsKey('ScanRoot')) { $checkOptions.ScanRoot = $ScanRoot } $report = Invoke-TempxCheck @checkOptions $reportText = Format-TempxReport -Report $report -MaxRows $MaxRows if (-not $NoClipboard) { try { Set-Clipboard -Value $reportText -ErrorAction Stop Write-Host "TEMPX PC check complete. $($reportText.Length) characters copied to clipboard." Write-Host 'Press Ctrl+V to paste the report. No report file was created.' } catch { Write-Warning "Clipboard failed: $($_.Exception.Message). The report is printed below." if (-not $PassThru) { $reportText } } } elseif (-not $PassThru) { $reportText } if ($PassThru) { $report } } @args