<# Name: SafeToBankCheck.ps1 (website edition of ATI-Safe-To-Bank-Check.ps1) Version: 1.0-web Date: 2026-10-03 Author: Adkins Technologies (safetobank.com / techtreats.info) Website edition: The 12 checks are exactly the same as ATI-Safe-To-Bank-Check.ps1 v1.0. The only addition: at the end it also saves a small results file, SafeToBank-Results.json, on your Desktop (and a copy next to the log), so you can see your results as a big, easy-to-read report at https://safetobank.com/report. That page reads the file inside your own browser; nothing is uploaded. Public internet addresses, e-mail addresses, and user folder names are hidden in the results file. When run by a person (not with -NoPause), it opens https://safetobank.com/report in your web browser at the end; the script itself still sends nothing. What it does: A quick "is this PC safe for online banking right now?" check. It looks at 12 things: antivirus, firewall, Windows Update, the hosts file, DNS servers, proxy settings, remote-control software, browser notification permissions and extensions, recent virus detections, Secure Boot / TPM, Wi-Fi security, and SmartScreen. Each item shows PASS, WARNING, or FAIL with one plain-English line telling you what to do. Read-only: This script ONLY READS settings. It makes NO changes to your PC, installs nothing, and sends NOTHING over the internet. The only files it writes are its own log, report, and results file, saved in C:\ProgramData\AdkinsTech\Logs (or your TEMP folder if that is not allowed), plus SafeToBank-Results.json on your Desktop. How to run: - Right-click the file > Run with PowerShell or - powershell -ExecutionPolicy Bypass -File .\SafeToBankCheck.ps1 - Add -NoPause to skip the "Press Enter" prompt (for RMM / unattended use). -NoPause also skips opening the web browser. - Add -NoBrowser to skip opening https://safetobank.com/report at the end. - Add -ResultsFolder to save SafeToBank-Results.json somewhere other than the Desktop. Works as a standard user. A few items (Secure Boot, TPM, Defender history) show more detail when run as administrator; otherwise they say "needs admin". Exit codes (for RMM checks): 0 = all good, 1 = warnings, 2 = at least one FAIL #> [CmdletBinding()] param( # Skip the "Press Enter to finish" prompt at the end (for RMM / unattended use) [switch]$NoPause, # Website edition: do not open https://safetobank.com/report at the end [switch]$NoBrowser, # Website edition: folder for SafeToBank-Results.json (default: your Desktop) [string]$ResultsFolder = '' ) # --------------------------------------------------------------------------- # Setup: names, counters, and the log folder # --------------------------------------------------------------------------- # Name and time stamp used for the log and report file names $ScriptName = 'ATI-Safe-To-Bank-Check' $Stamp = Get-Date -Format 'yyyyMMdd-HHmmss' # Counters for the summary and a list of lines for the plain-text report $script:Counts = @{ PASS = 0; WARNING = 0; FAIL = 0; INFO = 0 } $script:ReportLines = New-Object System.Collections.ArrayList # Website edition: one record per check (filled in by Write-Section and Add-Result) for the results file $script:Checks = New-Object System.Collections.ArrayList $script:CurrentCheck = $null # Pick the log folder: ProgramData\AdkinsTech\Logs, or TEMP if that cannot be created $LogDir = $null try { $LogDir = Join-Path $env:ProgramData 'AdkinsTech\Logs'; $null = New-Item -Path $LogDir -ItemType Directory -Force -ErrorAction Stop } catch { $LogDir = $null } if (-not $LogDir) { if ($env:TEMP) { $LogDir = $env:TEMP } else { $LogDir = [System.IO.Path]::GetTempPath() } } # Start the transcript (a copy of everything shown on screen); fall back to TEMP if the first spot fails $TranscriptPath = Join-Path $LogDir ("{0}-{1}.txt" -f $ScriptName, $Stamp) $TranscriptOn = $false try { $null = Start-Transcript -Path $TranscriptPath -ErrorAction Stop; $TranscriptOn = $true } catch { $TranscriptOn = $false } if (-not $TranscriptOn) { $LogDir = [System.IO.Path]::GetTempPath(); $TranscriptPath = Join-Path $LogDir ("{0}-{1}.txt" -f $ScriptName, $Stamp) } if (-not $TranscriptOn) { try { $null = Start-Transcript -Path $TranscriptPath -ErrorAction Stop; $TranscriptOn = $true } catch { $TranscriptOn = $false } } # --------------------------------------------------------------------------- # Helper functions # --------------------------------------------------------------------------- function Write-Section { param([int]$Number, [string]$Title) # Print a cyan section header and keep a copy for the report $Header = ('=== {0}. {1} ===' -f $Number, $Title) Write-Host '' Write-Host $Header -ForegroundColor Cyan $null = $script:ReportLines.Add('') $null = $script:ReportLines.Add($Header) # Website edition: start a new record for this check $script:CurrentCheck = [pscustomobject]@{ Number = $Number; Title = $Title; Results = (New-Object System.Collections.ArrayList) } $null = $script:Checks.Add($script:CurrentCheck) } function Add-Result { param([string]$Status, [string]$Message) # Choose the color for this status $Color = switch ($Status) { 'PASS' { 'Green' } 'WARNING' { 'Yellow' } 'FAIL' { 'Red' } default { 'Gray' } } # Print the line and keep a copy for the report $Line = (' {0,-8} {1}' -f $Status, $Message) Write-Host $Line -ForegroundColor $Color $null = $script:ReportLines.Add($Line) # Count it for the summary if ($script:Counts.ContainsKey($Status)) { $script:Counts[$Status]++ } # Website edition: keep a structured copy for the results file if ($script:CurrentCheck) { $null = $script:CurrentCheck.Results.Add([pscustomobject]@{ Status = $Status; Message = $Message }) } } function Get-ShortError { param($ErrorRecord) # Turn an error into one short, readable line $Text = '' try { $Text = (("$($ErrorRecord.Exception.Message)" -split "`r?`n")[0]).Trim() } catch { $Text = "$ErrorRecord" } # A missing command usually means this is not a normal Windows 10/11 PC try { if ($ErrorRecord.Exception -is [System.Management.Automation.CommandNotFoundException]) { $Text = ("the Windows command '{0}' is not available on this PC" -f $ErrorRecord.Exception.CommandName) } } catch { $null = $_ } # Keep it short if (-not $Text) { $Text = 'unknown error' } if ($Text.Length -gt 140) { $Text = $Text.Substring(0, 137) + '...' } return $Text } function Write-CheckError { param($ErrorRecord, [string]$What = 'this item') # Access-denied errors mean the check needs an administrator window; that is not a failure $Msg = Get-ShortError $ErrorRecord if ($Msg -match 'denied|0x80070005|Unauthorized|administrator|elevat') { Add-Result 'INFO' ("Could not check {0}: needs admin (run PowerShell as administrator to include it)." -f $What) } else { Add-Result 'WARNING' ("Could not check {0}: {1}." -f $What, $Msg) } } function Invoke-Check { param([int]$Number, [string]$Title, [scriptblock]$Body) # Print the header, then run the check; any error becomes a "could not check" warning instead of a crash Write-Section -Number $Number -Title $Title try { & $Body } catch { Write-CheckError $_ $Title } } function Assert-Registry { # Stop the check early if the Windows registry is not available (for example, not running on Windows) if (-not (Get-PSDrive -Name HKLM -ErrorAction SilentlyContinue)) { throw 'the Windows registry is not available on this system' } } function Get-RegValue { param([string]$Path, [string]$Name) # Read one registry value; return $null if the key or value does not exist $Value = $null try { $Value = (Get-ItemProperty -Path $Path -Name $Name -ErrorAction Stop).$Name } catch { $Value = $null } return $Value } function Read-TextFileShared { param([string]$Path) # Read a text file without locking it, so files in use (like browser settings) can still be read $Stream = $null try { $Stream = [System.IO.File]::Open($Path, 'Open', 'Read', 'ReadWrite'); $Reader = New-Object System.IO.StreamReader($Stream, $true); return $Reader.ReadToEnd() } finally { if ($Stream) { $Stream.Dispose() } } } function ConvertFrom-JsonSafe { param([string]$Text) # Try the normal JSON reader first try { return ($Text | ConvertFrom-Json -ErrorAction Stop) } catch { $null = $_ } # Big browser files can exceed the PowerShell 5.1 JSON size limit, so try the .NET reader with a larger limit try { Add-Type -AssemblyName System.Web.Extensions -ErrorAction Stop; $Js = New-Object System.Web.Script.Serialization.JavaScriptSerializer; $Js.MaxJsonLength = [int]::MaxValue; return $Js.DeserializeObject($Text) } catch { return $null } } function Get-JsonKeys { param($Object) # List the property names of a JSON object, whichever reader produced it if ($null -eq $Object) { return @() } if ($Object -is [System.Collections.IDictionary]) { return @($Object.Keys) } else { return @($Object.PSObject.Properties | ForEach-Object { $_.Name }) } } function Get-JsonValue { param($Object, [string]$Key) # Read one property of a JSON object, whichever reader produced it if ($null -eq $Object) { return $null } if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($Key)) { return $Object[$Key] } else { return $null } } $Prop = $Object.PSObject.Properties[$Key] if ($Prop) { return $Prop.Value } else { return $null } } function Get-JsonPath { param($Object, [string[]]$Keys) # Walk down several levels of a JSON object, stopping safely if a level is missing $Current = $Object foreach ($Key in $Keys) { $Current = Get-JsonValue $Current $Key; if ($null -eq $Current) { return $null } } return $Current } function Test-PrivateIPv4 { param([string]$Address) # Return $true for home/office (private), loopback, link-local, and carrier-grade NAT ranges $Ip = $null if (-not [System.Net.IPAddress]::TryParse($Address, [ref]$Ip)) { return $false } $B = $Ip.GetAddressBytes() if ($B.Count -ne 4) { return $false } if ($B[0] -eq 10 -or $B[0] -eq 127) { return $true } if ($B[0] -eq 172 -and $B[1] -ge 16 -and $B[1] -le 31) { return $true } if ($B[0] -eq 192 -and $B[1] -eq 168) { return $true } if ($B[0] -eq 169 -and $B[1] -eq 254) { return $true } if ($B[0] -eq 100 -and $B[1] -ge 64 -and $B[1] -le 127) { return $true } return $false } function Write-ThirdPartyAv { param($Av) # Security Center packs "on/off" and "up to date" into one number $State = 0 try { $State = [int]$Av.productState } catch { $State = 0 } $IsOn = (($State -band 0x1000) -ne 0) $IsCurrent = (($State -band 0x10) -eq 0) if (-not $IsOn) { Add-Result 'FAIL' ("{0} is your antivirus but it is turned OFF. Open it and turn protection on before banking." -f $Av.displayName) } elseif (-not $IsCurrent) { Add-Result 'WARNING' ("{0} is on, but its virus definitions are out of date. Open it and run an update." -f $Av.displayName) } else { Add-Result 'PASS' ("{0} is your antivirus, it is on, and it reports up to date (Defender is standing by, which is normal)." -f $Av.displayName) } } # --------------------------------------------------------------------------- # Website edition helpers: build the results file for safetobank.com/report # --------------------------------------------------------------------------- function Protect-ResultText { param([string]$Text) # Hide anything personal before it goes into the results file (the screen and log still show everything) if (-not $Text) { return '' } # Public IPv4 addresses are hidden; home/office (private) addresses and the 0.0.0.0 "block" address are kept $Text = [regex]::Replace($Text, '(? Virus & threat protection and turn it on.' } if ($Mp.RealTimeProtectionEnabled) { Add-Result 'PASS' 'Real-time protection is on.' } else { Add-Result 'FAIL' 'Real-time protection is OFF. Open Windows Security > Virus & threat protection > Manage settings and turn it on.' } # Virus definitions should be recent $Age = [int]$Mp.AntivirusSignatureAge if ($Age -gt 7) { Add-Result 'FAIL' ("Virus definitions are {0} days old. Open Windows Security > Protection updates > Check for updates." -f $Age) } elseif ($Age -gt 2) { Add-Result 'WARNING' ("Virus definitions are {0} days old. Open Windows Security > Protection updates > Check for updates." -f $Age) } else { Add-Result 'PASS' ("Virus definitions are current ({0} day(s) old)." -f $Age) } } # --------------------------------------------------------------------------- # 2. Firewall # --------------------------------------------------------------------------- Invoke-Check 2 'Windows Firewall' { # Read the three firewall profiles (Domain, Private, Public) $Profiles = @(Get-NetFirewallProfile -ErrorAction Stop) if ($Profiles.Count -eq 0) { throw 'no firewall profiles were returned' } # See if another firewall product is registered with Security Center $FwProducts = @() try { $FwProducts = @(Get-CimInstance -Namespace 'root/SecurityCenter2' -ClassName FirewallProduct -ErrorAction Stop | Where-Object { $_.displayName }) } catch { $FwProducts = @() } $FwNames = ($FwProducts | ForEach-Object { $_.displayName }) -join ', ' # Report each profile foreach ($P in $Profiles) { # A profile that is off is a FAIL, unless another firewall product says it is covering it $IsOn = ("$($P.Enabled)" -eq 'True') if ($IsOn) { Add-Result 'PASS' ("Firewall is on for the {0} network profile." -f $P.Name) } elseif ($FwProducts.Count -gt 0) { Add-Result 'WARNING' ("Windows Firewall is off for the {0} profile, but {1} is registered and may be protecting you. Make sure it is on." -f $P.Name, $FwNames) } else { Add-Result 'FAIL' ("Firewall is OFF for the {0} profile. Open Windows Security > Firewall & network protection and turn it on." -f $P.Name) } } } # --------------------------------------------------------------------------- # 3. Windows Update # --------------------------------------------------------------------------- function Test-LastHotfix { # Get installed updates that have a date, newest first $Fixes = @(Get-HotFix -ErrorAction Stop) $Dated = @() foreach ($F in $Fixes) { $When = $null; try { $When = [datetime]$F.InstalledOn } catch { $When = $null }; if ($When) { $Dated += [pscustomobject]@{ Id = $F.HotFixID; When = $When } } } if ($Dated.Count -eq 0) { throw 'no dated updates were found' } $Latest = $Dated | Sort-Object When -Descending | Select-Object -First 1 # Work out how many days since the last update $Days = [int]((Get-Date) - $Latest.When).TotalDays $Msg = ("Last update {0} was installed on {1} ({2} days ago)." -f $Latest.Id, $Latest.When.ToString('yyyy-MM-dd'), $Days) if ($Days -gt 60) { Add-Result 'FAIL' ("$Msg Run Settings > Windows Update > Check for updates and install everything.") } elseif ($Days -gt 35) { Add-Result 'WARNING' ("$Msg Run Settings > Windows Update > Check for updates.") } else { Add-Result 'PASS' $Msg } } function Test-RebootPending { # Look for the registry flags Windows sets when a restart is needed to finish updates Assert-Registry $Cbs = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' $Wu = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' if ($Cbs -or $Wu) { Add-Result 'WARNING' 'A restart is waiting to finish installing updates. Save your work and restart before banking.' } else { Add-Result 'PASS' 'No restart is pending for updates.' } } Invoke-Check 3 'Windows Update' { # Check the last update date and pending restarts separately, so one problem does not hide the other try { Test-LastHotfix } catch { Write-CheckError $_ 'the last update date' } try { Test-RebootPending } catch { Write-CheckError $_ 'for a pending restart' } } # --------------------------------------------------------------------------- # 4. Hosts file # --------------------------------------------------------------------------- Invoke-Check 4 'Hosts file' { # Find the hosts file if (-not $env:SystemRoot) { throw 'this does not look like Windows (SystemRoot is not set)' } $HostsPath = Join-Path $env:SystemRoot 'System32\drivers\etc\hosts' if (-not (Test-Path -LiteralPath $HostsPath)) { Add-Result 'INFO' 'No hosts file was found. That is unusual but not dangerous.'; return } # Words that suggest banking, payment, or very common sites $Keywords = 'bank|chase|wellsfargo|citi|paypal|venmo|capitalone|usbank|schwab|fidelity|vanguard|amex|americanexpress|discover|creditunion|google|microsoft|apple|amazon' # Addresses that only block a site (they do not redirect it anywhere) $BlockAddresses = @('0.0.0.0', '127.0.0.1', '::1', '::') # Read each line, skip comments and blanks, and split into address + names $Entries = @() foreach ($Raw in (Get-Content -LiteralPath $HostsPath -ErrorAction Stop)) { # Remove comments and extra spaces $Line = ($Raw -replace '#.*$', '').Trim() if (-not $Line) { continue } # First word is the address, the rest are site names $Parts = @($Line -split '\s+') if ($Parts.Count -lt 2) { continue } $Entries += [pscustomobject]@{ Address = $Parts[0]; Names = @($Parts[1..($Parts.Count - 1)]) } } # Leave out the normal localhost lines $Other = @($Entries | Where-Object { @($_.Names | Where-Object { $_ -notmatch '^(localhost|localhost\.localdomain|ip6-localhost|ip6-loopback)$' }).Count -gt 0 }) # List what is there (first 30) foreach ($E in ($Other | Select-Object -First 30)) { Add-Result 'INFO' ("hosts entry: {0} -> {1}" -f ($E.Names -join ' '), $E.Address) } if ($Other.Count -gt 30) { Add-Result 'INFO' ("...and {0} more entries." -f ($Other.Count - 30)) } # Look for banking or common sites sent to a real address (a classic way to send you to a fake site) $Bad = @() foreach ($E in $Other) { foreach ($N in $E.Names) { if ($N -match $Keywords -and $BlockAddresses -notcontains $E.Address) { $Bad += ("{0} -> {1}" -f $N, $E.Address) } } } foreach ($B in $Bad) { Add-Result 'FAIL' ("The hosts file redirects {0}. This can send you to a fake site. Have a trusted technician remove this line." -f $B) } if ($Bad.Count -gt 0) { return } # Otherwise just note how many extra entries there are if ($Other.Count -gt 0) { Add-Result 'WARNING' ("The hosts file has {0} entry(ies) besides localhost. Some ad-blockers and security tools add blocking lines on purpose; if you did not expect this, ask your IT provider." -f $Other.Count) } else { Add-Result 'PASS' 'The hosts file only has the normal default entries.' } } # --------------------------------------------------------------------------- # 5. DNS servers # --------------------------------------------------------------------------- Invoke-Check 5 'DNS servers' { # Well-known public DNS services (Cloudflare, Google, Quad9, OpenDNS, AdGuard) $KnownGood = @('1.1.1.1', '1.0.0.1', '8.8.8.8', '8.8.4.4', '9.9.9.9', '149.112.112.112', '208.67.222.222', '208.67.220.220', '94.140.14.14', '94.140.15.15') # Find network adapters that are connected $UpIndexes = @(Get-NetAdapter -ErrorAction Stop | Where-Object { $_.Status -eq 'Up' } | ForEach-Object { $_.ifIndex }) # Read the IPv4 DNS servers for those adapters $DnsList = @(Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { ($UpIndexes -contains $_.InterfaceIndex) -and $_.ServerAddresses }) if ($DnsList.Count -eq 0) { Add-Result 'INFO' 'No DNS servers were found on connected adapters.'; return } $Unknown = 0 foreach ($D in $DnsList) { foreach ($S in $D.ServerAddresses) { # Private/router addresses and well-known services are fine; anything else gets a second look if ($KnownGood -contains $S) { Add-Result 'INFO' ("{0}: {1} (well-known public DNS)" -f $D.InterfaceAlias, $S) } elseif (Test-PrivateIPv4 $S) { Add-Result 'INFO' ("{0}: {1} (your router or local network)" -f $D.InterfaceAlias, $S) } else { $Unknown++; Add-Result 'WARNING' ("{0}: {1} is not a well-known DNS service. It may just be your internet provider; if you did not set it and do not recognize it, have it checked." -f $D.InterfaceAlias, $S) } } } if ($Unknown -eq 0) { Add-Result 'PASS' 'DNS servers look normal.' } } # --------------------------------------------------------------------------- # 6. Proxy settings # --------------------------------------------------------------------------- function Test-UserProxy { # Read your user's internet proxy settings and return how many warnings were raised Assert-Registry $Key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' $ProxyEnable = Get-RegValue $Key 'ProxyEnable' $ProxyServer = Get-RegValue $Key 'ProxyServer' $Pac = Get-RegValue $Key 'AutoConfigURL' $Found = 0 # A turned-on proxy or a PAC script can route your web traffic through someone else if ($ProxyEnable -eq 1) { $Found++; Add-Result 'WARNING' ("A proxy server is turned on: {0}. If you or your IT did not set this, turn it off in Settings > Network & internet > Proxy." -f $ProxyServer) } if ($Pac) { $Found++; Add-Result 'WARNING' ("A proxy setup script (PAC) is set: {0}. If you or your IT did not set this, turn it off in Settings > Network & internet > Proxy." -f $Pac) } if ($Found -eq 0) { Add-Result 'PASS' 'No proxy is set for your user.' } } function Test-WinHttpProxy { # Read the system-wide (WinHTTP) proxy with netsh (read-only "show" command) $WinHttp = (& netsh.exe winhttp show proxy 2>&1 | Out-String) $Match = [regex]::Match($WinHttp, 'Proxy Server\(s\)\s*:\s*(\S+)') if ($WinHttp -match 'Direct access') { Add-Result 'PASS' 'No system-wide (WinHTTP) proxy is set.' } elseif ($Match.Success) { Add-Result 'WARNING' ("A system-wide (WinHTTP) proxy is set: {0}. If your IT did not set this, have it checked." -f $Match.Groups[1].Value) } else { Add-Result 'INFO' 'Could not read the system-wide (WinHTTP) proxy setting (this can happen on non-English Windows).' } } Invoke-Check 6 'Proxy settings' { # Check the user proxy and the system proxy separately, so one problem does not hide the other try { Test-UserProxy } catch { Write-CheckError $_ 'your user proxy settings' } try { Test-WinHttpProxy } catch { Write-CheckError $_ 'the system-wide (WinHTTP) proxy' } } # --------------------------------------------------------------------------- # 7. Remote-control software # --------------------------------------------------------------------------- Invoke-Check 7 'Remote-control software' { # Remote tools that scammers often ask people to install (name patterns for processes, services, and installed programs) $RemoteTools = @( @{ Name = 'AnyDesk'; Pattern = 'AnyDesk' }, @{ Name = 'TeamViewer'; Pattern = 'TeamViewer' }, @{ Name = 'ScreenConnect / ConnectWise Control'; Pattern = 'ScreenConnect|ConnectWise ?Control' }, @{ Name = 'LogMeIn'; Pattern = 'LogMeIn|LMIGuardian|LMI_Rescue|LMIIgnition' }, @{ Name = 'GoTo (GoToAssist / GoTo Resolve / GoToMyPC)'; Pattern = 'GoToAssist|GoTo ?Resolve|GoToMyPC|g2ax' }, @{ Name = 'Splashtop'; Pattern = 'Splashtop|^SRService$|^SRServer$|^SRManager$|^SRAgent$' }, @{ Name = 'RustDesk'; Pattern = 'RustDesk' }, @{ Name = 'UltraViewer'; Pattern = 'UltraViewer' }, @{ Name = 'Supremo'; Pattern = '^Supremo' }, @{ Name = 'Ammyy Admin'; Pattern = 'Ammyy|^AA_v3' }, @{ Name = 'RemotePC'; Pattern = 'RemotePC|^RPCService$|^RPCSuite' }, @{ Name = 'Zoho Assist'; Pattern = 'Zoho ?Assist|^ZA_Connect|ZAService' }, @{ Name = 'Quick Assist'; Pattern = '^QuickAssist$'; RunningOnly = $true } ) # Tools used by IT providers (including Adkins Technologies) for managed support $ManagedPattern = 'N-able|N-sight|Take ?Control|BASupSrvc|BASupApp|BASupSys|BASupTS|BeAnywhere|Advanced ?Monitoring ?Agent|^winagent$|SolarWinds MSP' # Read running programs (works on any system) $Procs = @(Get-Process -ErrorAction Stop) # Read installed services (Windows only) $Svcs = @(); $SvcOk = $true try { $Svcs = @(Get-Service -ErrorAction SilentlyContinue) } catch { $SvcOk = $false; Write-CheckError $_ 'installed services' } if ($SvcOk -and $Svcs.Count -eq 0) { $SvcOk = $false; Add-Result 'WARNING' 'Could not check installed services: the list came back empty.' } # Read installed program names from the uninstall list (Windows only) $Apps = @() try { Assert-Registry; $Apps = @(Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | ForEach-Object { "$($_.DisplayName)" }) } catch { $Apps = @() } # Note managed IT tools as information only $Managed = @() $Managed += @($Procs | Where-Object { $_.ProcessName -match $ManagedPattern } | ForEach-Object { $_.ProcessName }) $Managed += @($Svcs | Where-Object { $_.Name -match $ManagedPattern -or $_.DisplayName -match $ManagedPattern } | ForEach-Object { $_.DisplayName }) $Managed += @($Apps | Where-Object { $_ -match $ManagedPattern }) $Managed = @($Managed | Sort-Object -Unique) if ($Managed.Count -gt 0) { Add-Result 'INFO' ("Managed by your IT provider: {0}. These are remote-support tools used by IT companies such as Adkins Technologies. If you do not have an IT provider, ask someone you trust about them." -f ($Managed -join ', ')) } # Check each remote tool $Hits = 0 foreach ($T in $RemoteTools) { # Find matching running programs, services, and installed programs (ignoring managed IT tools) $P = @($Procs | Where-Object { $_.ProcessName -match $T.Pattern -and $_.ProcessName -notmatch $ManagedPattern }) $S = @($Svcs | Where-Object { ($_.Name -match $T.Pattern -or $_.DisplayName -match $T.Pattern) -and ($_.Name -notmatch $ManagedPattern) -and ($_.DisplayName -notmatch $ManagedPattern) }) $A = @($Apps | Where-Object { $_ -match $T.Pattern -and $_ -notmatch $ManagedPattern }) $Running = ($P.Count -gt 0) -or (@($S | Where-Object { "$($_.Status)" -eq 'Running' }).Count -gt 0) $Installed = (($S.Count -gt 0) -or ($A.Count -gt 0)) -and (-not $T.RunningOnly) # Running right now is a FAIL; installed but not running is a WARNING if ($Running) { $Hits++; Add-Result 'FAIL' ("{0} is RUNNING right now. If someone claiming to be your bank or tech support asked you to install this, hang up and close it now." -f $T.Name) } elseif ($Installed) { $Hits++; Add-Result 'WARNING' ("{0} is installed but not running. If you do not use it, uninstall it from Settings > Apps." -f $T.Name) } } if ($Hits -eq 0 -and $SvcOk) { Add-Result 'PASS' 'No common remote-control tools are running or installed.' } if ($Hits -eq 0 -and -not $SvcOk) { Add-Result 'PASS' 'No common remote-control tools are running (installed services could not be checked).' } # Check whether Remote Desktop (RDP) is accepting connections try { Assert-Registry; $Deny = Get-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' 'fDenyTSConnections'; if ($Deny -eq 0) { Add-Result 'WARNING' 'Remote Desktop is turned ON. If you do not use it, turn it off in Settings > System > Remote Desktop.' } elseif ($Deny -eq 1) { Add-Result 'PASS' 'Remote Desktop is off.' } else { Add-Result 'INFO' 'Remote Desktop setting not found.' } } catch { Write-CheckError $_ 'Remote Desktop' } } # --------------------------------------------------------------------------- # 8. Browser notifications and extensions # --------------------------------------------------------------------------- function Get-ExtensionName { param([string]$VersionDir, $Manifest) # Use the plain name from the manifest when it has one $Name = "$(Get-JsonValue $Manifest 'name')" $MsgMatch = [regex]::Match($Name, '^__MSG_(.+)__$') if (-not $MsgMatch.Success) { return $Name } # Otherwise look the name up in the extension's English (or default) language file $MsgKey = $MsgMatch.Groups[1].Value $Locales = @('en', 'en_US', 'en_GB', "$(Get-JsonValue $Manifest 'default_locale')") | Where-Object { $_ } foreach ($L in $Locales) { # Read the language file for this locale $MsgFile = Join-Path $VersionDir ("_locales\{0}\messages.json" -f $L) if (-not (Test-Path -LiteralPath $MsgFile)) { continue } $Msgs = $null try { $Msgs = ConvertFrom-JsonSafe (Read-TextFileShared $MsgFile) } catch { $Msgs = $null } # Keys are not case sensitive, so compare without case foreach ($K in (Get-JsonKeys $Msgs)) { if ($K -ieq $MsgKey) { $Text = Get-JsonValue (Get-JsonValue $Msgs $K) 'message'; if ($Text) { return "$Text" } } } } return $Name } function Test-BrowserProfile { param([string]$BrowserName, [System.IO.DirectoryInfo]$ProfileDir) # Read this profile's settings file $PrefFile = Join-Path $ProfileDir.FullName 'Preferences' $Prefs = $null if (Test-Path -LiteralPath $PrefFile) { $Prefs = ConvertFrom-JsonSafe (Read-TextFileShared $PrefFile) } $ProfileName = "$(Get-JsonPath $Prefs @('profile', 'name'))" if (-not $ProfileName) { $ProfileName = $ProfileDir.Name } $Label = ("{0} ({1})" -f $BrowserName, $ProfileName) # List sites allowed to send notifications (setting 1 = allow) $NotifyRoot = Get-JsonPath $Prefs @('profile', 'content_settings', 'exceptions', 'notifications') $Allowed = @() foreach ($Site in (Get-JsonKeys $NotifyRoot)) { $Setting = Get-JsonValue (Get-JsonValue $NotifyRoot $Site) 'setting'; if ("$Setting" -eq '1') { $Allowed += ($Site -replace ',\*$', '') } } if ($Allowed.Count -gt 0) { Add-Result 'WARNING' ("{0}: {1} site(s) may show you notifications. Fake virus pop-ups get in this way. If you do not know a site below, block it in the browser's Settings > Privacy > Site permissions > Notifications." -f $Label, $Allowed.Count) } else { Add-Result 'PASS' ("{0}: no sites are allowed to send notifications." -f $Label) } foreach ($Site in $Allowed) { Add-Result 'INFO' (" allowed to notify: {0}" -f $Site) } # List installed extensions by reading each extension's manifest $ExtRoot = Join-Path $ProfileDir.FullName 'Extensions' if (-not (Test-Path -LiteralPath $ExtRoot)) { Add-Result 'INFO' ("{0}: no extensions installed." -f $Label); return } $ExtList = @() foreach ($IdDir in @(Get-ChildItem -LiteralPath $ExtRoot -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'Temp' })) { # Use the newest version folder $VerDir = Get-ChildItem -LiteralPath $IdDir.FullName -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 if (-not $VerDir) { continue } $ManifestFile = Join-Path $VerDir.FullName 'manifest.json' if (-not (Test-Path -LiteralPath $ManifestFile)) { continue } $Manifest = $null try { $Manifest = ConvertFrom-JsonSafe (Read-TextFileShared $ManifestFile) } catch { $Manifest = $null } if (-not $Manifest) { continue } # Work out the display name and whether it updates from an official store $Name = Get-ExtensionName $VerDir.FullName $Manifest if (-not $Name) { $Name = $IdDir.Name } $UpdateUrl = "$(Get-JsonValue $Manifest 'update_url')" $FromStore = ($UpdateUrl -match 'clients2\.google\.com|edge\.microsoft\.com|microsoftedge\.microsoft\.com') $ExtList += [pscustomobject]@{ Name = $Name; Id = $IdDir.Name; FromStore = $FromStore } } Add-Result 'INFO' ("{0}: {1} extension(s) installed." -f $Label, $ExtList.Count) foreach ($X in ($ExtList | Sort-Object Name)) { if ($X.FromStore) { Add-Result 'INFO' (" extension: {0}" -f $X.Name) } else { Add-Result 'WARNING' (" extension: {0} (id {1}) does not update from the official store. If you or your IT did not add it on purpose, remove it." -f $X.Name, $X.Id) } } } Invoke-Check 8 'Browser notifications and extensions (Edge and Chrome)' { # Find the Edge and Chrome data folders for this user if (-not $env:LOCALAPPDATA) { throw 'LOCALAPPDATA is not set (this does not look like a Windows user profile)' } $Browsers = @( @{ Name = 'Microsoft Edge'; Root = (Join-Path $env:LOCALAPPDATA 'Microsoft\Edge\User Data') }, @{ Name = 'Google Chrome'; Root = (Join-Path $env:LOCALAPPDATA 'Google\Chrome\User Data') } ) foreach ($B in $Browsers) { # Skip browsers that are not set up for this user if (-not (Test-Path -LiteralPath $B.Root)) { Add-Result 'INFO' ("{0} is not set up for this user." -f $B.Name); continue } # Check every profile (Default, Profile 1, Profile 2, ...) $ProfileDirs = @(Get-ChildItem -LiteralPath $B.Root -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq 'Default' -or $_.Name -like 'Profile *' }) if ($ProfileDirs.Count -eq 0) { Add-Result 'INFO' ("{0}: no profiles found." -f $B.Name); continue } foreach ($Pd in $ProfileDirs) { try { Test-BrowserProfile $B.Name $Pd } catch { Write-CheckError $_ ("{0} profile '{1}'" -f $B.Name, $Pd.Name) } } } } # --------------------------------------------------------------------------- # 9. Recent Defender detections # --------------------------------------------------------------------------- Invoke-Check 9 'Recent virus detections (last 14 days)' { # Read Defender's detection history and keep the last 14 days $Since = (Get-Date).AddDays(-14) $Detections = @(Get-MpThreatDetection -ErrorAction Stop | Where-Object { $_.InitialDetectionTime -ge $Since }) # Look up the threat names $NameById = @{} try { foreach ($T in @(Get-MpThreat -ErrorAction Stop)) { $NameById["$($T.ThreatID)"] = $T.ThreatName } } catch { $NameById = @{} } $Names = @($Detections | ForEach-Object { $N = $NameById["$($_.ThreatID)"]; if ($N) { $N } else { "Threat ID $($_.ThreatID)" } } | Sort-Object -Unique) # Report what was found if ($Detections.Count -gt 0) { Add-Result 'WARNING' ("Defender found {0} threat(s) recently: {1}. Open Windows Security > Protection history and make sure each one was removed." -f $Detections.Count, ($Names -join ', ')) } elseif ($IsAdmin) { Add-Result 'PASS' 'No virus detections in the last 14 days.' } else { Add-Result 'PASS' 'No virus detections visible to this user in the last 14 days (run as administrator for the full history).' } } # --------------------------------------------------------------------------- # 10. Secure Boot and TPM # --------------------------------------------------------------------------- function Test-SecureBootState { # As administrator, ask the firmware directly if ($IsAdmin) { try { $On = Confirm-SecureBootUEFI -ErrorAction Stop; if ($On) { Add-Result 'INFO' 'Secure Boot is on.' } else { Add-Result 'WARNING' 'Secure Boot is off. It helps block boot-level malware; a technician can turn it on in the PC firmware (BIOS) settings.' }; return } catch { if ("$_" -match 'not supported') { Add-Result 'INFO' 'This PC starts in legacy BIOS mode, so Secure Boot is not available.'; return } } } # As a standard user, read the status Windows keeps in the registry Assert-Registry $State = Get-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State' 'UEFISecureBootEnabled' if ($State -eq 1) { Add-Result 'INFO' 'Secure Boot is on.' } elseif ($State -eq 0) { Add-Result 'WARNING' 'Secure Boot is off. It helps block boot-level malware; a technician can turn it on in the PC firmware (BIOS) settings.' } else { Add-Result 'INFO' 'Secure Boot status: needs admin (or this PC uses legacy BIOS).' } } function Test-TpmState { # Get-Tpm only works as administrator if (-not $IsAdmin) { Add-Result 'INFO' 'TPM (security chip) status: needs admin. Run PowerShell as administrator to include it.'; return } $Tpm = Get-Tpm -ErrorAction Stop if (-not $Tpm.TpmPresent) { Add-Result 'WARNING' 'No TPM security chip was found (or it is turned off in the firmware).' } elseif (-not $Tpm.TpmReady) { Add-Result 'WARNING' 'A TPM security chip is present but not ready. A technician can finish setting it up.' } else { Add-Result 'INFO' 'TPM security chip is present and ready.' } } Invoke-Check 10 'Secure Boot and TPM' { # These are information only; neither one can fail the check try { Test-SecureBootState } catch { Write-CheckError $_ 'Secure Boot' } try { Test-TpmState } catch { Write-CheckError $_ 'the TPM' } } # --------------------------------------------------------------------------- # 11. Wi-Fi security # --------------------------------------------------------------------------- Invoke-Check 11 'Wi-Fi security' { # Ask Windows about the wireless connection $Out = @(& netsh.exe wlan show interfaces 2>&1 | ForEach-Object { "$_" }) $All = $Out -join "`n" # Newer Windows needs Location turned on before it will show Wi-Fi details if ($All -match 'location permission|Location services') { Add-Result 'INFO' 'Windows needs Location turned on to show Wi-Fi details, so this was skipped. Make sure your Wi-Fi asks for a password.'; return } if ($All -match 'no wireless interface|not running') { Add-Result 'INFO' 'No Wi-Fi adapter in use (this PC is probably wired).'; return } # Walk through the output, one adapter at a time $Found = 0; $State = ''; $Ssid = ''; $Auth = '' foreach ($L in ($Out + @(' Name : end'))) { # A new "Name" line starts the next adapter, so report the one we just finished if ($L -match '^\s*Name\s*:') { if ($State -match '^connected' -and $Ssid) { $Found++; if ($Auth -match '^Open') { Add-Result 'FAIL' ("Connected to Wi-Fi '{0}' with NO password (Open). Do not bank on open Wi-Fi; use a secured network or your phone's hotspot." -f $Ssid) } else { Add-Result 'PASS' ("Connected to Wi-Fi '{0}' using {1}." -f $Ssid, $Auth) } }; $State = ''; $Ssid = ''; $Auth = ''; continue } # Remember the fields we care about if ($L -match '^\s*State\s*:\s*(.+)$') { $State = $Matches[1].Trim() } if ($L -match '^\s*SSID\s*:\s*(.+)$') { $Ssid = $Matches[1].Trim() } if ($L -match '^\s*Authentication\s*:\s*(.+)$') { $Auth = $Matches[1].Trim() } } if ($Found -eq 0) { Add-Result 'INFO' 'Not connected to Wi-Fi right now (or the details could not be read).' } } # --------------------------------------------------------------------------- # 12. SmartScreen # --------------------------------------------------------------------------- Invoke-Check 12 'SmartScreen (warns about bad downloads and sites)' { # Read the Windows and Edge SmartScreen settings Assert-Registry $Off = 0 $Policy = Get-RegValue 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System' 'EnableSmartScreen' $Explorer = Get-RegValue 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer' 'SmartScreenEnabled' $EdgePolicyLm = Get-RegValue 'HKLM:\SOFTWARE\Policies\Microsoft\Edge' 'SmartScreenEnabled' $EdgePolicyCu = Get-RegValue 'HKCU:\SOFTWARE\Policies\Microsoft\Edge' 'SmartScreenEnabled' $EdgeUser = Get-RegValue 'HKCU:\Software\Microsoft\Edge\SmartScreenEnabled' '(default)' # Warn only when something is explicitly turned off if ($Policy -eq 0) { $Off++; Add-Result 'WARNING' 'Windows SmartScreen is turned off by policy. Ask your IT provider why.' } if ("$Explorer" -eq 'Off') { $Off++; Add-Result 'WARNING' 'Windows SmartScreen for apps and files is off. Turn it on in Windows Security > App & browser control > Reputation-based protection.' } if ($EdgePolicyLm -eq 0 -or $EdgePolicyCu -eq 0) { $Off++; Add-Result 'WARNING' 'Edge SmartScreen is turned off by policy. Ask your IT provider why.' } if ($EdgeUser -eq 0) { $Off++; Add-Result 'WARNING' 'Edge SmartScreen is off. Turn it on in Edge Settings > Privacy, search, and services > Security.' } if ($Off -eq 0) { Add-Result 'PASS' 'SmartScreen is not turned off.' } } # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- # Pick the verdict, color, and exit code $ExitCode = 0; $Verdict = 'SAFE TO BANK'; $VerdictColor = 'Green' if ($script:Counts.FAIL -gt 0) { $ExitCode = 2; $Verdict = 'DO NOT BANK ON THIS PC UNTIL THE RED ITEMS ARE FIXED'; $VerdictColor = 'Red' } elseif ($script:Counts.WARNING -gt 0) { $ExitCode = 1; $Verdict = 'CHECK THE WARNINGS ABOVE'; $VerdictColor = 'Yellow' } # Print a big banner $Bar = '#' * 72 $CountLine = ("PASS: {0} WARN: {1} FAIL: {2} (INFO: {3})" -f $script:Counts.PASS, $script:Counts.WARNING, $script:Counts.FAIL, $script:Counts.INFO) Write-Host '' Write-Host $Bar -ForegroundColor $VerdictColor Write-Host ('# ' + $Verdict) -ForegroundColor $VerdictColor Write-Host ('# ' + $CountLine) -ForegroundColor $VerdictColor Write-Host $Bar -ForegroundColor $VerdictColor Write-Host 'Note: this check looks for common problems. It cannot promise a PC is free of all malware.' # Save a plain-text report next to the transcript $null = $script:ReportLines.Add('') $null = $script:ReportLines.Add($Bar) $null = $script:ReportLines.Add('# ' + $Verdict) $null = $script:ReportLines.Add('# ' + $CountLine) $null = $script:ReportLines.Add($Bar) $ReportPath = Join-Path $LogDir ("{0}-{1}-Report.txt" -f $ScriptName, $Stamp) try { Set-Content -LiteralPath $ReportPath -Value $script:ReportLines -Encoding UTF8 -ErrorAction Stop; Write-Host ("Report saved to: {0}" -f $ReportPath) } catch { Write-Host ("Could not save the report ({0})." -f (Get-ShortError $_)) -ForegroundColor Yellow } if ($TranscriptOn) { Write-Host ("Full log saved to: {0}" -f $TranscriptPath) } # Website edition: save SafeToBank-Results.json for the report page at safetobank.com/report $WindowsText = '' if ($Os) { $WindowsText = ("{0} (build {1})" -f $Os.Caption, $Os.BuildNumber) } $ResultsPath = $null try { $ResultsPath = Save-ResultsFile (Get-ResultsObject -Verdict $Verdict -ExitCode $ExitCode -IsAdmin $IsAdmin -WindowsText $WindowsText) } catch { Write-Host ("Could not save the results file ({0})." -f (Get-ShortError $_)) -ForegroundColor Yellow } if ($ResultsPath) { Write-Host '' Write-Host 'NEXT STEP: see your results as a big, easy-to-read report' -ForegroundColor Cyan Write-Host ("Your results file is saved here: {0}" -f $ResultsPath) -ForegroundColor Cyan Write-Host 'Now drag SafeToBank-Results.json onto safetobank.com (https://safetobank.com/report),' -ForegroundColor Cyan Write-Host 'or click "Choose my results file" on that page. The page reads it on your PC; nothing is uploaded.' -ForegroundColor Cyan # Open the report page for a person at the keyboard (not for RMM / -NoPause / -NoBrowser runs) if (-not $NoBrowser -and -not $NoPause -and [Environment]::UserInteractive) { try { Start-Process 'https://safetobank.com/report' -ErrorAction Stop; Write-Host 'Opening https://safetobank.com/report in your web browser...' } catch { Write-Host 'Open https://safetobank.com/report in your web browser.' } } } # Stop the transcript if ($TranscriptOn) { try { $null = Stop-Transcript } catch { $null = $_ } } # Wait for Enter unless -NoPause was used or nobody is at the screen if (-not $NoPause -and [Environment]::UserInteractive) { $null = Read-Host 'Press Enter to finish' } # Exit code for RMM: 0 = all good, 1 = warnings, 2 = any FAIL exit $ExitCode