@echo off
setlocal
title ImposterShield free computer scan
cd /d "%~dp0"

set "PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
if not exist "%PS%" set "PS=powershell.exe"

set "IMPOSTERSHIELD_HOME=%~dp0"
set "ISTMP=%TEMP%\ImposterShield-Scan-%RANDOM%%RANDOM%.ps1"

"%PS%" -NoProfile -ExecutionPolicy Bypass -Command "$t=[IO.File]::ReadAllText('%~f0',[Text.Encoding]::UTF8); $m='#@SCAN-PAYLOAD@'; $i=$t.LastIndexOf($m); if($i -lt 0){exit 9}; [IO.File]::WriteAllText('%ISTMP%', $t.Substring($i+$m.Length), (New-Object Text.UTF8Encoding $false))"
if not exist "%ISTMP%" (
  echo.
  echo   This file did not unpack correctly. It may have been damaged by the download.
  echo   Please download it again from https://impostershield.com/scan
  echo.
  pause
  exit /b 1
)

"%PS%" -NoProfile -ExecutionPolicy Bypass -File "%ISTMP%"
set RC=%ERRORLEVEL%
del "%ISTMP%" >nul 2>&1

echo.
if not "%RC%"=="0" (
  echo   The scan did not finish cleanly ^(code %RC%^).
  echo   Send a photo of this window to hello@impostershield.com and we will fix it.
) else (
  echo   Done. Your report is on your Desktop.
)
echo.
pause
exit /b %RC%

#@SCAN-PAYLOAD@<#
  ImposterShield free computer scan
  https://impostershield.com

  Looks at what is running on this computer and what starts with it, and tells you
  in plain English whether anything here can watch your screen, control your mouse,
  or act on its own.

  Nothing leaves this computer. There is no account, no upload, and no network call.
  You can read every line of this file before you run it. That is the point.

  Usage:
    powershell -ExecutionPolicy Bypass -File scan.ps1
    powershell -ExecutionPolicy Bypass -File scan.ps1 -Json out.json -NoOpen
#>

[CmdletBinding()]
param(
  [string]$ReportPath,
  [string]$Json,
  [switch]$NoOpen,
  [switch]$Quiet
)

$ErrorActionPreference = 'Stop'
$ScanVersion = '0.1.3'

# Anything that goes wrong anywhere in this script lands here. Without it a terminating
# error just closes the window, which is indistinguishable from the scan having done
# nothing at all - that is exactly how the first failure got reported.
trap {
  # The single-file .cmd unpacks this into TEMP, so it hands us the folder the person
  # actually downloaded to. That is where they will go looking for the file.
  $logDir = if ($env:IMPOSTERSHIELD_HOME -and (Test-Path $env:IMPOSTERSHIELD_HOME)) { $env:IMPOSTERSHIELD_HOME }
            elseif ($PSScriptRoot) { $PSScriptRoot }
            else { $env:TEMP }
  $log = Join-Path $logDir 'ImposterShield-scan-error.txt'
  $detail = @(
    "ImposterShield scan $ScanVersion"
    "When    : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
    "Windows : $([Environment]::OSVersion.VersionString)"
    "PS      : $($PSVersionTable.PSVersion)"
    ""
    "Error   : $($_.Exception.Message)"
    "Where   : $($_.InvocationInfo.PositionMessage)"
    "Type    : $($_.Exception.GetType().FullName)"
  ) -join [Environment]::NewLine
  try { Set-Content -LiteralPath $log -Value $detail -Encoding UTF8 } catch { }

  Write-Host ''
  Write-Host '  The scan hit a problem and stopped.' -ForegroundColor Red
  Write-Host "  $($_.Exception.Message)" -ForegroundColor DarkYellow
  Write-Host ''
  Write-Host '  The details were written to:' -ForegroundColor Gray
  Write-Host "  $log" -ForegroundColor Gray
  Write-Host '  Send that file to hello@impostershield.com and we will fix it.' -ForegroundColor Gray
  Write-Host ''
  exit 1
}

# ImposterShield scan catalog
# Every entry answers three questions in plain English: what is it, why do we care,
# what should you do. If an entry cannot answer all three, it does not belong here.
#
# kind:
#   remote     someone elsewhere can see or drive this screen
#   monitor    something is recording the screen, keys, or activity
#   agent      an AI model or AI agent running on this machine
#   automation software that drives the mouse, keyboard, or a browser by itself
#
# level:
#   alert   tell the person immediately, even if they installed it on purpose
#   note    worth knowing about, not alarming on its own

# How a catalog term is allowed to match. Getting this wrong in either direction is
# the whole ballgame, so it lives in one place and test-catalog.ps1 tests this exact
# function rather than a copy of it.
#
#   left  (?<![A-Za-z0-9])  the term has to start a word. Without this, "aider"
#                           matches "ARC Raiders" and "cline" matches "decline".
#   right (?![a-z0-9])      case sensitive on purpose. A capital letter is allowed to
#                           follow, because real binaries are named SplashtopStreamer
#                           and AnyDeskMSI. A lowercase letter is not, so "radmin"
#                           cannot swallow "Radminton".
function New-CatalogMatcher($terms) {
  $alts = ($terms | ForEach-Object { '(?i:' + [regex]::Escape($_) + ')' }) -join '|'
  return New-Object regex("(?<![A-Za-z0-9])(?:$alts)(?![a-z0-9])",
    [Text.RegularExpressions.RegexOptions]::Compiled)
}

$script:Catalog = @(

  # ---------------------------------------------------------------- remote access
  # The tools that actually drain bank accounts. A "Microsoft support" caller talks
  # someone into installing one of these, then watches them log into their bank.
  @{ id='anydesk';      name='AnyDesk';               kind='remote'; level='alert'
     match=@('anydesk')
     what='AnyDesk lets another person see this screen and use the mouse and keyboard from anywhere in the world.' }

  @{ id='teamviewer';   name='TeamViewer';            kind='remote'; level='alert'
     match=@('teamviewer','tv_w32','tv_x64')
     what='TeamViewer lets another person see this screen and control this computer over the internet.' }

  @{ id='screenconnect';name='ScreenConnect / ConnectWise Control'; kind='remote'; level='alert'
     match=@('screenconnect','connectwisecontrol','connectwise control')
     what='ScreenConnect gives a remote technician full control of this computer. It is normal on a work machine and a red flag on a home one.' }

  @{ id='rustdesk';     name='RustDesk';              kind='remote'; level='alert'
     match=@('rustdesk')
     what='RustDesk is free remote-control software. It is popular with support scammers because it installs in seconds and needs no account.' }

  @{ id='ultraviewer';  name='UltraViewer';           kind='remote'; level='alert'
     match=@('ultraviewer')
     what='UltraViewer hands control of this computer to someone at another computer.' }

  @{ id='supremo';      name='Supremo';               kind='remote'; level='alert'
     match=@('supremo')
     what='Supremo is remote-control software. It needs no installation, which is exactly why phone scammers reach for it.' }

  @{ id='ammyy';        name='Ammyy Admin';           kind='remote'; level='alert'
     match=@('ammyy','aa_v3')
     what='Ammyy Admin is remote-control software that has been used in phone scams for years. Almost nobody installs it for a good reason.' }

  @{ id='netsupport';   name='NetSupport Manager';    kind='remote'; level='alert'
     match=@('netsupport','client32','pcictlui')
     what='NetSupport Manager is classroom and helpdesk control software. It is also one of the most commonly abused remote-control tools on home computers.' }

  @{ id='aeroadmin';    name='AeroAdmin';             kind='remote'; level='alert'
     match=@('aeroadmin')
     what='AeroAdmin gives someone else control of this screen with no setup and no account.' }

  @{ id='remoteutils';  name='Remote Utilities';      kind='remote'; level='alert'
     match=@('remote utilities','rutserv','rfusclient')
     what='Remote Utilities runs quietly in the background and lets someone connect to this computer whenever they like.' }

  @{ id='dwagent';      name='DWAgent';               kind='remote'; level='alert'
     match=@('dwagent')
     what='DWAgent is a remote-support agent that stays installed and waits for someone to connect.' }

  @{ id='getscreen';    name='Getscreen.me';          kind='remote'; level='alert'
     match=@('getscreen')
     what='Getscreen lets someone open this computer from a web browser somewhere else.' }

  @{ id='iperius';      name='Iperius Remote';        kind='remote'; level='alert'
     match=@('iperius')
     what='Iperius Remote allows another person to view and control this desktop.' }

  @{ id='zohoassist';   name='Zoho Assist';           kind='remote'; level='alert'
     match=@('zohoassist','zoho assist','zaservice')
     what='Zoho Assist is remote-support software. Fine if your own IT person set it up, worth questioning otherwise.' }

  @{ id='logmein';      name='LogMeIn';               kind='remote'; level='alert'
     match=@('logmein','lmiguardian')
     what='LogMeIn keeps this computer permanently reachable from the internet by whoever holds the account.' }

  @{ id='gotoassist';   name='GoTo Resolve / GoToAssist'; kind='remote'; level='alert'
     match=@('gotoassist','goto resolve')
     what='GoToAssist gives a remote agent control of this computer during a support session, and sometimes after it.' }

  @{ id='splashtop';    name='Splashtop';             kind='remote'; level='alert'
     match=@('splashtop','srserver','srmanager')
     what='Splashtop lets someone sign in and use this computer remotely.' }

  @{ id='radmin';       name='Radmin';                kind='remote'; level='alert'
     match=@('radmin','rserver3','famitrfc')
     what='Radmin is remote-control software that runs as a background service.' }

  @{ id='vnc';          name='VNC remote desktop';    kind='remote'; level='alert'
     match=@('tightvnc','ultravnc','realvnc','tigervnc','winvnc','vncserver','vncviewer')
     what='VNC software shares this screen over the network. It often runs with no visible window at all.' }

  @{ id='chromerd';     name='Chrome Remote Desktop'; kind='remote'; level='note'
     match=@('remoting_host','chromoting','chrome remote desktop')
     what='Chrome Remote Desktop lets you, or anyone with your Google password, control this computer from another device.' }

  @{ id='quickassist';  name='Quick Assist';          kind='remote'; level='note'
     match=@('quickassist')
     what='Quick Assist is built into Windows and lets someone control this screen after you read them a code. Scammers ask for that code by phone.' }

  @{ id='parsec';       name='Parsec';                kind='remote'; level='note'
     match=@('parsecd','parsec.exe')
     what='Parsec streams this desktop to another device. Most people use it for games.' }

  # ------------------------------------------------------------ managed / RMM agents
  # Legitimate on a work laptop. On a personal machine, someone else is administering it.
  @{ id='rmm';          name='Remote management agent'; kind='remote'; level='note'
     match=@('ateraagent','syncro','kaseya','agentmon','itarian','comodo remote','level.io','action1','ninjarmm','ninjarmmagent','pulseway','datto rmm','centrastage','screenmeet','tacticalrmm','meshagent')
     what='This is business software that lets an IT company install programs and run commands on this computer without asking each time.' }

  # ------------------------------------------------------------------- monitoring
  @{ id='stalkerware';  name='Activity monitoring software'; kind='monitor'; level='alert'
     match=@('spyrix','refog','actualspy','ardamax','perfectkeylogger','realtime-spy','flexispy','mspy','hoverwatch','kidlogger','wolfeye','snoopza','elite keylogger','all in one keylogger')
     what='This software records what is typed and what appears on the screen and sends it to someone else. It is sold as parental or employee monitoring and is also what an abuser installs.' }

  @{ id='worktracking'; name='Employee tracking software'; kind='monitor'; level='note'
     match=@('activtrak','hubstaff','teramind','veriato','timedoctor','time doctor','workpuls','insightful','desktime','controlio','interguard')
     what='This takes screenshots and logs activity for an employer. Expected on a work computer, not on a personal one.' }

  @{ id='screenrec';    name='Screen recording software'; kind='monitor'; level='note'
     match=@('obs64','obs32','obs-studio','bandicam','camtasia','sharex','action.exe','fraps','xsplit','streamlabs')
     what='This can record everything on this screen. Almost always something the owner installed on purpose.' }

  # ------------------------------------------------------------------- AI agents
  # Not dangerous by default. The point is that people do not know how much of this
  # is on their machine, or what it can reach.
  @{ id='ollama';       name='Ollama';                kind='agent'; level='note'
     match=@('ollama')
     what='Ollama runs AI language models directly on this computer. It usually listens for requests from other programs in the background.' }

  @{ id='lmstudio';     name='LM Studio';             kind='agent'; level='note'
     match=@('lm studio','lmstudio','lms.exe')
     what='LM Studio runs AI models on this machine and can serve them to other apps.' }

  @{ id='localllm';     name='Local AI model server'; kind='agent'; level='note'
     match=@('llama-server','llamacpp','llama.cpp','koboldcpp','text-generation-webui','gpt4all','jan.exe','anythingllm','open-webui','localai','vllm','oobabooga')
     what='This is an AI model running on this computer rather than in the cloud.' }

  @{ id='imagegen';     name='AI image generator';    kind='agent'; level='note'
     match=@('comfyui','stable-diffusion','stablediffusion','automatic1111','invokeai','fooocus','sd.next')
     what='This creates AI images on this computer. Worth knowing about: images made here have no watermark and no AI stamp.' }

  @{ id='aidesktop';    name='AI assistant app';      kind='agent'; level='note'
     match=@('claude.exe','claude desktop','chatgpt.exe','chatgpt desktop','openai desktop','perplexity','grok desktop')
     what='An AI assistant app is installed. Some of these can read the screen or open files when you ask them to.' }

  @{ id='wincopilot';   name='Microsoft Copilot';     kind='agent'; level='note'
     match=@('copilot')
     what='Copilot is the AI assistant Microsoft builds into Windows. It is here whether or not anyone chose to install it, and what you type into it goes to Microsoft.' }

  @{ id='aicoder';      name='AI coding tool';        kind='agent'; level='note'
     match=@('cursor.exe','windsurf','aider','continue.exe','cline','opendevin','devin','codeium','tabnine','warp.exe')
     what='This is a developer tool with an AI agent that can read and change files on this computer.' }

  @{ id='aiagent';      name='Autonomous AI agent';   kind='agent'; level='alert'
     match=@('autogpt','auto-gpt','agentgpt','babyagi','crewai','superagi','open-interpreter','openinterpreter','gpt-engineer')
     what='This is an AI agent built to act on its own: run commands, open programs, and use the internet without being asked each time.' }

  # ----------------------------------------------------------------- automation
  @{ id='webdriver';    name='Browser automation driver'; kind='automation'; level='alert'
     match=@('chromedriver','geckodriver','msedgedriver','operadriver','safaridriver','selenium-server','iedriverserver')
     what='This program drives a web browser by itself, clicking and typing as if a person were doing it.' }

  @{ id='browserauto';  name='Browser automation framework'; kind='automation'; level='note'
     match=@('playwright','puppeteer','node_modules\playwright','headless_shell')
     what='This is software used to control a browser automatically. Common on a developer machine, unusual anywhere else.' }

  @{ id='rpa';          name='Desktop automation software'; kind='automation'; level='note'
     match=@('uipath','pad.console','power automate','automationdesktop','blueprism','automation anywhere','winautomation')
     what='This can operate this computer on its own, moving the mouse and typing into programs.' }

  @{ id='macro';        name='Macro / scripting tool'; kind='automation'; level='note'
     match=@('autohotkey','autoit3','au3','pulover','macro recorder','ghostmouse','tinytask')
     what='This can replay mouse clicks and keystrokes. Most people use it for shortcuts, and it is also how cheating and click-fraud tools work.' }
)

# Programs that are supposed to autostart from a user folder. Without this list,
# every ordinary Discord or Spotify install looks like a finding, and a scan that
# cries wolf is worse than no scan.
$script:AutostartAllowlist = @(
  'onedrive','discord','spotify','slack','zoom','dropbox','google drive','googledrive',
  'steam','epicgames','epic games','ea desktop','ealauncher','riotclient','riot games',
  'battle.net','gog galaxy','overwolf','razer','logitech','lghub','corsair','icue',
  'signalrgb','nvidia','amd ','realtek','synaptics','elan','intel','microsoft','google',
  'teams','edge','chrome','firefox','opera','brave','1password','bitwarden','lastpass',
  'dashlane','grammarly','f.lux','flux','notion','obsidian','todoist','rainmeter',
  'webex','citrix','goodnotes','adobe','creative cloud','ccxprocess','java','oracle',
  'python','node','git','docker','wsl','vmware','virtualbox','malwarebytes','bitdefender',
  'norton','mcafee','avast','avg','eset','kaspersky','sophos','crowdstrike','sentinelone'
)
if (-not $script:Catalog) {
  $catalogFile = Join-Path $PSScriptRoot 'catalog.ps1'
  if (Test-Path $catalogFile) { . $catalogFile }
  else { throw 'Catalog not found. Run the packaged scan.ps1, or keep catalog.ps1 next to this file.' }
}

# ---------------------------------------------------------------- console output

$script:Steps = 0
function Say($text, $color = 'Gray') {
  if (-not $Quiet) { Write-Host $text -ForegroundColor $color }
}
function Step($text) {
  $script:Steps++
  if (-not $Quiet) { Write-Host ("  {0}. {1}" -f $script:Steps, $text) -ForegroundColor DarkGray }
}

if (-not $Quiet) {
  Write-Host ''
  Write-Host '  ImposterShield' -ForegroundColor Yellow -NoNewline
  Write-Host "  free computer scan  v$ScanVersion" -ForegroundColor DarkGray
  Write-Host '  Nothing you see here is sent anywhere.' -ForegroundColor DarkGray
  Write-Host ''
}

# ------------------------------------------------------------------- helpers

$script:SigCache = @{}
function Get-Sig($path) {
  if (-not $path) { return $null }
  if ($script:SigCache.ContainsKey($path)) { return $script:SigCache[$path] }
  $result = [pscustomobject]@{ Signed = $false; Publisher = $null }
  try {
    $s = Get-AuthenticodeSignature -LiteralPath $path -ErrorAction Stop
    if ($s.Status -eq 'Valid' -and $s.SignerCertificate) {
      $subject = $s.SignerCertificate.Subject
      $cn = ($subject -split ',' | Where-Object { $_.Trim() -like 'CN=*' } | Select-Object -First 1)
      $result.Signed = $true
      $result.Publisher = if ($cn) { $cn.Trim().Substring(3).Trim('"') } else { $subject }
    }
  } catch { }
  $script:SigCache[$path] = $result
  return $result
}

function Test-Allowlisted($text) {
  if (-not $text) { return $false }
  $t = $text.ToLowerInvariant()
  foreach ($a in $script:AutostartAllowlist) { if ($t.Contains($a)) { return $true } }
  return $false
}

# Findings are keyed by catalog id so that one program seen in four places
# (running, installed, autostarting, listening) reads as one finding with four
# lines of evidence, not four separate scares.
$script:Findings = @{}
function Add-Evidence($entry, $line) {
  if (-not $script:Findings.ContainsKey($entry.id)) {
    $script:Findings[$entry.id] = [pscustomobject]@{
      id       = $entry.id
      name     = $entry.name
      kind     = $entry.kind
      level    = $entry.level
      what     = $entry.what
      evidence = New-Object System.Collections.ArrayList
    }
  }
  $f = $script:Findings[$entry.id]
  if (-not $f.evidence.Contains($line)) { [void]$f.evidence.Add($line) }
}

# Loose findings are the checks that are not about a named program:
# a script that starts with Windows, a browser being driven, Recall taking snapshots.
$script:Loose = New-Object System.Collections.ArrayList
function Add-Finding($id, $level, $kind, $name, $what, $evidence, $action) {
  [void]$script:Loose.Add([pscustomobject]@{
    id = $id; level = $level; kind = $kind; name = $name; what = $what
    action = $action; evidence = @($evidence)
  })
}

# One compiled alternation per entry, built by the shared matcher in catalog.ps1.
$script:CatalogRx = @{}
foreach ($entry in $script:Catalog) {
  $script:CatalogRx[$entry.id] = New-CatalogMatcher $entry.match
}

function Match-Catalog($haystack, $evidenceLine) {
  if (-not $haystack) { return }
  foreach ($entry in $script:Catalog) {
    if ($script:CatalogRx[$entry.id].IsMatch($haystack)) { Add-Evidence $entry $evidenceLine }
  }
}

function Fmt-Time($dt) {
  if (-not $dt) { return $null }
  try { return (Get-Date $dt -Format 'h:mm tt') } catch { return $null }
}

# --------------------------------------------------------------- 1. processes

Step 'Looking at everything running right now'

$procs = @()
try {
  $procs = Get-CimInstance Win32_Process -ErrorAction Stop |
    Select-Object ProcessId, Name, ExecutablePath, CommandLine, CreationDate
} catch {
  Say '     (could not read the process list on this computer)' 'DarkYellow'
}

$procByPid = @{}
foreach ($p in $procs) { $procByPid[[int]$p.ProcessId] = $p }

# Group before matching. Three copies of the same program is one fact about this
# computer, not three findings.
foreach ($g in ($procs | Group-Object Name)) {
  $first   = $g.Group | Sort-Object CreationDate | Select-Object -First 1
  $started = Fmt-Time $first.CreationDate
  $copies  = if ($g.Count -gt 1) { " ($($g.Count) copies)" } else { '' }
  $line    = if ($started) { "Running right now as $($g.Name)$copies, started at $started" }
             else { "Running right now as $($g.Name)$copies" }
  Match-Catalog $g.Name $line
  foreach ($path in ($g.Group | Select-Object -ExpandProperty ExecutablePath -Unique)) {
    if ($path) { Match-Catalog $path $line }
  }
}

# ---------------------------------------------------------------- 2. services

Step 'Checking background services'

try {
  foreach ($s in (Get-CimInstance Win32_Service -ErrorAction Stop | Select-Object Name, DisplayName, PathName, State, StartMode)) {
    # A service that is stopped and only starts when something asks for it is dormant.
    # Reporting those turns every dormant Windows component into a scare.
    if ($s.State -ne 'Running' -and $s.StartMode -ne 'Auto') { continue }
    $state = if ($s.State -eq 'Running') { 'running' } else { $s.State.ToLower() }
    $auto  = if ($s.StartMode -eq 'Auto') { ' and starts with Windows' } else { '' }
    $line  = "Installed as a background service ""$($s.DisplayName)"", currently $state$auto"
    Match-Catalog $s.Name $line
    Match-Catalog $s.DisplayName $line
    Match-Catalog $s.PathName $line
  }
} catch { }

# ------------------------------------------------------------ 3. installed apps

Step 'Reading the list of installed programs'

$uninstallKeys = @(
  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
try {
  $apps = Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue |
    Where-Object { $_.DisplayName }
  foreach ($a in $apps) {
    $when = $null
    if ($a.InstallDate -and $a.InstallDate -match '^\d{8}$') {
      try { $when = ([datetime]::ParseExact($a.InstallDate, 'yyyyMMdd', $null)).ToString('d MMMM yyyy') } catch { }
    }
    $line = if ($when) { "Installed on this computer as ""$($a.DisplayName)"" on $when" }
            else       { "Installed on this computer as ""$($a.DisplayName)""" }
    Match-Catalog $a.DisplayName $line
    if ($a.InstallLocation) { Match-Catalog $a.InstallLocation $line }
  }
} catch { }

# --------------------------------------------------------------- 4. autostart

Step 'Checking what starts up with Windows'

$runKeys = @(
  @{ path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run';               scope='every user' },
  @{ path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce';           scope='every user' },
  @{ path='HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run';   scope='every user' },
  @{ path='HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run';               scope='you' },
  @{ path='HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce';           scope='you' }
)

$autoruns = New-Object System.Collections.ArrayList
foreach ($k in $runKeys) {
  try {
    $item = Get-Item $k.path -ErrorAction SilentlyContinue
    if (-not $item) { continue }
    foreach ($valueName in $item.GetValueNames()) {
      if (-not $valueName) { continue }
      [void]$autoruns.Add([pscustomobject]@{
        Label = $valueName; Command = [string]$item.GetValue($valueName)
        Source = "the startup list for $($k.scope)"
      })
    }
  } catch { }
}

$startupFolders = @(
  @{ path = Join-Path $env:APPDATA      'Microsoft\Windows\Start Menu\Programs\Startup'; scope='you' },
  @{ path = Join-Path $env:ProgramData  'Microsoft\Windows\Start Menu\Programs\Startup'; scope='every user' }
)
foreach ($d in $startupFolders) {
  try {
    Get-ChildItem $d.path -File -ErrorAction SilentlyContinue | ForEach-Object {
      [void]$autoruns.Add([pscustomobject]@{
        Label = $_.Name; Command = $_.FullName
        Source = "the Startup folder for $($d.scope)"
      })
    }
  } catch { }
}

# Pull the executable out of a command line so signatures can be checked.
function Get-ExeFromCommand($cmd) {
  if (-not $cmd) { return $null }
  $c = $cmd.Trim()
  if ($c.StartsWith('"')) {
    $end = $c.IndexOf('"', 1)
    if ($end -gt 1) { return $c.Substring(1, $end - 1) }
  }
  $m = [regex]::Match($c, '^(.*?\.(?:exe|vbs|bat|cmd|ps1|js|jar|scr|com|lnk))(\s|$)', 'IgnoreCase')
  if ($m.Success) { return $m.Groups[1].Value }
  return ($c -split '\s+')[0]
}

$scriptStarters   = New-Object System.Collections.ArrayList
$unsignedStarters = New-Object System.Collections.ArrayList

foreach ($a in $autoruns) {
  $line = "Starts automatically from $($a.Source), listed as ""$($a.Label)"""
  Match-Catalog $a.Label   $line
  Match-Catalog $a.Command $line

  $exe = Get-ExeFromCommand $a.Command
  if (-not $exe) { continue }
  $exe = [Environment]::ExpandEnvironmentVariables($exe)

  if ($exe -match '\.(vbs|bat|cmd|ps1|js|jse|vbe|wsf)$') {
    [void]$scriptStarters.Add("$($a.Label) runs $exe, from $($a.Source)")
    continue
  }

  if (Test-Allowlisted $a.Label)   { continue }
  if (Test-Allowlisted $exe)       { continue }
  if (-not (Test-Path -LiteralPath $exe -PathType Leaf -ErrorAction SilentlyContinue)) { continue }

  # Only user-writable locations are worth flagging. A program in Program Files
  # needed an administrator to get there; a program in AppData or Downloads did not.
  $userWritable = $exe -match '\\AppData\\|\\Temp\\|\\Downloads\\|\\Users\\Public\\|\\ProgramData\\'
  if (-not $userWritable) { continue }

  $sig = Get-Sig $exe
  if ($sig -and $sig.Signed) {
    if (-not (Test-Allowlisted $sig.Publisher)) {
      # Signed but by nobody we know: worth a line, not an alarm.
      [void]$unsignedStarters.Add("$($a.Label) runs $exe, signed by $($sig.Publisher), from $($a.Source)")
    }
  } else {
    [void]$unsignedStarters.Add("$($a.Label) runs $exe, with no signature saying who made it, from $($a.Source)")
  }
}

if ($scriptStarters.Count -gt 0) {
  Add-Finding 'startup-scripts' 'note' 'automation' 'A script runs every time Windows starts' `
    'A script is a small text file of instructions. Scripts are how a lot of useful things get automated, and also how something unwanted keeps itself alive after a restart.' `
    $scriptStarters `
    'Right-click the file, choose Open with, and pick Notepad. You will see the instructions in readable text. If you recognise what it is doing, leave it. If it is downloading something or contacting an address you do not know, delete the file from the Startup folder.'
}
if ($unsignedStarters.Count -gt 0) {
  Add-Finding 'unknown-starters' 'note' 'automation' 'Something starts with Windows from a folder anyone can write to' `
    'These programs launch themselves at startup from a personal folder rather than an installed program folder, and none of them carry a signature from a company we recognise. That is normal for small tools and hobby software, and it is also where unwanted software likes to hide.' `
    $unsignedStarters `
    'Look at the names. If you recognise the program and meant to install it, nothing here needs doing. If a name means nothing to you, search it before you remove it, then take it out of the startup list in Task Manager under the Startup apps tab.'
}

# --------------------------------------------------------- 5. scheduled tasks

Step 'Checking scheduled tasks'

try {
  foreach ($t in (Get-ScheduledTask -ErrorAction SilentlyContinue)) {
    if ($t.TaskPath -like '\Microsoft\*') { continue }
    $line = "Set to run on a schedule as the task ""$($t.TaskName)"""
    Match-Catalog $t.TaskName $line
    foreach ($act in $t.Actions) {
      if ($act.Execute)   { Match-Catalog $act.Execute   $line }
      if ($act.Arguments) { Match-Catalog $act.Arguments $line }
    }
  }
} catch { }

# ------------------------------------------------------- 6. browser being driven

Step 'Checking whether anything is driving your browser'

$driven = New-Object System.Collections.ArrayList
foreach ($p in $procs) {
  if ($p.Name -notmatch '^(chrome|msedge|brave|opera|firefox|chromium)\.exe$') { continue }
  $cmd = $p.CommandLine
  if (-not $cmd) { continue }
  if ($cmd -match '--remote-debugging-port=(\d+)') {
    [void]$driven.Add("$($p.Name) is running with a remote control port open on port $($Matches[1])")
  } elseif ($cmd -match '--remote-debugging-pipe') {
    [void]$driven.Add("$($p.Name) is running with a remote control channel open")
  }
  if ($cmd -match '--headless') {
    [void]$driven.Add("$($p.Name) is running invisibly, with no window on screen")
  }
  if ($cmd -match '--load-extension=([^\s""]+)') {
    [void]$driven.Add("$($p.Name) was launched with an add-on loaded from a folder: $($Matches[1])")
  }
}
if ($driven.Count -gt 0) {
  Add-Finding 'browser-driven' 'alert' 'automation' 'Your web browser can be controlled by another program' `
    'A browser started this way accepts commands from software on this computer. It can open pages, type into forms and click buttons with nobody touching the keyboard. Testing tools and developer setups do this deliberately.' `
    $driven `
    'If you write software or use automation tools, this is expected. If you do not, close the browser completely, including from the system tray, and open it again the normal way. Then check the Startup apps list for whatever launched it.'
}

# ----------------------------------------------------------- 7. what is listening

Step 'Checking what is accepting connections from outside'

try {
  $listening = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
    Where-Object { $_.LocalAddress -notin @('127.0.0.1', '::1') }

  $openDoors = New-Object System.Collections.ArrayList
  $seenPorts = @{}
  foreach ($c in $listening) {
    $p = $procByPid[[int]$c.OwningProcess]
    if (-not $p) { continue }
    $line = "$($p.Name) is accepting connections on port $($c.LocalPort)"
    Match-Catalog $p.Name $line
    if ($p.ExecutablePath) { Match-Catalog $p.ExecutablePath $line }

    if ($seenPorts.ContainsKey("$($p.Name):$($c.LocalPort)")) { continue }
    $seenPorts["$($p.Name):$($c.LocalPort)"] = $true

    if (-not $p.ExecutablePath) { continue }
    if ($p.ExecutablePath -like "$env:SystemRoot\*") { continue }
    if (Test-Allowlisted $p.ExecutablePath) { continue }

    $sig = Get-Sig $p.ExecutablePath
    if (-not $sig.Signed) {
      [void]$openDoors.Add("$($p.Name) is waiting for connections on port $($c.LocalPort), and carries no signature saying who made it ($($p.ExecutablePath))")
    }
  }

  if ($openDoors.Count -gt 0) {
    Add-Finding 'open-doors' 'note' 'remote' 'Something unidentified is waiting for connections' `
      'These programs are holding a door open on your network so other machines can reach them. Games, media servers and printers do this legitimately. The reason they are listed is that nothing in the file says who wrote them.' `
      $openDoors `
      'Not a problem by itself. Match each one to something you actually use. Anything you cannot place is worth searching by name.'
  }
} catch { }

# --------------------------------------------------------- 8. Windows Recall

Step 'Checking whether Windows is saving pictures of your screen'

try {
  $recallStore = Join-Path $env:LOCALAPPDATA 'CoreAIPlatform.00\UKP'
  if (Test-Path $recallStore) {
    $snaps = @(Get-ChildItem $recallStore -Recurse -Filter '*.jpg' -ErrorAction SilentlyContinue)
    if ($snaps.Count -gt 0) {
      $newest = ($snaps | Sort-Object LastWriteTime -Descending | Select-Object -First 1).LastWriteTime
      Add-Finding 'recall' 'note' 'monitor' 'Windows is taking pictures of your screen' `
        'This is Windows Recall, a feature built into Windows that saves snapshots of your screen so you can search back through what you were doing. The snapshots stay on this computer, but anyone who can get into this account can read them, including things you typed that were on screen at the time. You can turn it off in Settings under Privacy and security, then Recall and snapshots.' `
        @("$($snaps.Count) screen snapshots are stored on this computer", "The most recent one was saved $($newest.ToString('d MMMM yyyy, h:mm tt'))") `
        'If you use Recall and like it, leave it on. If you did not know it was running, open Settings, go to Privacy and security, then Recall and snapshots, and turn it off. Delete the snapshots already saved while you are there.'
    }
  }
} catch { }

# ------------------------------------------------------------------- results

Step 'Writing your report'

$all = @()
$all += $script:Findings.Values
$all += $script:Loose

$order = @{ remote = 0; monitor = 1; automation = 2; agent = 3 }
$all = $all | Sort-Object `
  @{ Expression = { if ($_.level -eq 'alert') { 0 } else { 1 } } }, `
  @{ Expression = { $order[[string]$_.kind] } }, `
  @{ Expression = { $_.name } }

$alerts = @($all | Where-Object { $_.level -eq 'alert' })
$notes  = @($all | Where-Object { $_.level -ne 'alert' })

function Get-Action($f) {
  if ($f.PSObject.Properties['action'] -and $f.action) { return $f.action }
  switch ($f.kind) {
    'remote' {
      if ($f.level -eq 'alert') {
        return 'If you installed this yourself, or your employer did, there is nothing wrong here. If somebody phoned you and talked you through installing it, treat this as urgent: unplug from the internet, remove the program, and change your bank and email passwords from a different device.'
      }
      return 'Worth knowing it is there. Remove it if nobody is using it.'
    }
    'monitor' {
      if ($f.level -eq 'alert') {
        return 'If you did not install this, someone else may be reading what you type. Do not change your passwords on this computer until it is removed, because the change itself would be recorded.'
      }
      return 'Fine if you know why it is there. Remove it if you do not.'
    }
    'agent' {
      return 'Nothing to do if you installed this on purpose. AI tools can often read files and the screen when you ask them to, so it is worth knowing which ones are here.'
    }
    'automation' {
      if ($f.level -eq 'alert') {
        return 'Software like this can act without you. If you did not set it up, close it and check what installed it.'
      }
      return 'Common on a computer used for work or development. Worth a second look on a family computer.'
    }
  }
  return 'Have a look and decide whether you recognise it.'
}

if (-not $Quiet) {
  Write-Host ''
  if ($alerts.Count -eq 0 -and $notes.Count -eq 0) {
    Write-Host '  Nothing found.' -ForegroundColor Green
    Write-Host '  No remote control software, no monitoring software, no AI agents.' -ForegroundColor DarkGray
  } else {
    if ($alerts.Count -gt 0) {
      Write-Host ("  {0} thing{1} worth your attention" -f $alerts.Count, $(if ($alerts.Count -eq 1) { '' } else { 's' })) -ForegroundColor Red
      foreach ($f in $alerts) { Write-Host "    - $($f.name)" -ForegroundColor Red }
    }
    if ($notes.Count -gt 0) {
      Write-Host ("  {0} thing{1} to know about" -f $notes.Count, $(if ($notes.Count -eq 1) { '' } else { 's' })) -ForegroundColor Yellow
      foreach ($f in $notes) { Write-Host "    - $($f.name)" -ForegroundColor DarkYellow }
    }
  }
  Write-Host ''
}

# ----------------------------------------------------------------- the report

function Esc($s) {
  if ($null -eq $s) { return '' }
  return ([string]$s).Replace('&', '&amp;').Replace('<', '&lt;').Replace('>', '&gt;').Replace('"', '&quot;')
}

$kindLabel = @{
  remote     = 'Remote control'
  monitor    = 'Watching this computer'
  agent      = 'AI on this computer'
  automation = 'Acts on its own'
}

$cards = New-Object System.Text.StringBuilder
foreach ($f in $all) {
  $cls = if ($f.level -eq 'alert') { 'alert' } else { 'note' }
  $tag = if ($f.level -eq 'alert') { 'Worth your attention' } else { 'Worth knowing' }
  [void]$cards.Append("<article class=""card $cls"">")
  [void]$cards.Append("<div class=""cardhead""><span class=""tag"">$tag</span><span class=""kind"">$(Esc $kindLabel[[string]$f.kind])</span></div>")
  [void]$cards.Append("<h3>$(Esc $f.name)</h3>")
  [void]$cards.Append("<p>$(Esc $f.what)</p>")
  [void]$cards.Append('<p class="lbl">Where we found it</p><ul>')
  foreach ($e in $f.evidence) { [void]$cards.Append("<li>$(Esc $e)</li>") }
  [void]$cards.Append('</ul>')
  [void]$cards.Append("<p class=""lbl"">What to do</p><p class=""action"">$(Esc (Get-Action $f))</p>")
  [void]$cards.Append('</article>')
}

$headline = if ($alerts.Count -gt 0) {
  "$($alerts.Count) thing$(if($alerts.Count -eq 1){''}else{'s'}) here can watch or control this computer."
} elseif ($notes.Count -gt 0) {
  "Nothing alarming. $($notes.Count) thing$(if($notes.Count -eq 1){' is'}else{'s are'}) worth knowing about."
} else {
  'Nothing found on this computer.'
}
$subline = if ($alerts.Count -gt 0) {
  'That does not automatically mean something is wrong. Read what each one is, then decide whether you put it there.'
} elseif ($notes.Count -gt 0) {
  'No remote control software and no monitoring software. The items below are things a person should simply know are on their own machine.'
} else {
  'No remote control software, no monitoring software, no AI agents, and nothing driving your browser. We looked at every running program, every background service, everything that starts with Windows, and everything accepting connections.'
}

$stamp = Get-Date -Format 'dddd d MMMM yyyy, h:mm tt'
$machine = "$env:COMPUTERNAME"

$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ImposterShield scan &mdash; $(Esc $machine)</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='26' font-size='26'>&#128737;</text></svg>">
<style>
  /* Warm light, matching impostershield.com. The report used to be near-black with amber,
     which is hazard-sign colouring - the wrong register for something that mostly tells a
     worried person their computer is fine. This should read like a letter, not an alarm. */
  :root{
    --bg:#FBF7F1; --bg-2:#FFFFFF; --line:#EAE0D4;
    --ink:#221E1A; --ink-2:#6B6158;
    --flag:#B35C00; --flag-fill:#F6A623; --real:#1F7A52; --alarm:#B3261E;
    --shadow:0 1px 2px rgba(80,60,35,.05), 0 10px 28px -14px rgba(80,60,35,.18);
  }
  *{box-sizing:border-box;margin:0;padding:0}
  body{background:var(--bg);color:var(--ink);font:400 18px/1.72 "Archivo","Segoe UI",-apple-system,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased}
  .wrap{max-width:820px;margin:0 auto;padding:0 24px}
  h1,h2,h3{letter-spacing:-.022em;line-height:1.1;font-weight:680}
  header{border-bottom:1px solid var(--line);padding:44px 0 40px}
  .brand{display:flex;align-items:center;gap:9px;font-weight:680;font-size:16px;color:var(--ink-2);margin-bottom:28px}
  .brand svg{width:20px;height:20px}
  h1{font-size:clamp(28px,5vw,40px);margin-bottom:14px}
  .sub{color:var(--ink-2);max-width:62ch}
  .meta{margin-top:26px;font-size:14px;color:var(--ink-2)}
  main{padding:36px 0 8px;display:grid;gap:18px}
  .card{background:var(--bg-2);border:1px solid var(--line);border-radius:16px;padding:26px 28px;box-shadow:var(--shadow)}
  .card.alert{border-color:rgba(179,38,30,.38);border-left:3px solid var(--alarm)}
  .cardhead{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap}
  .tag{font-size:12.5px;font-weight:680;padding:4px 10px;border-radius:6px;background:rgba(246,166,35,.18);color:#7A3E00}
  .card.alert .tag{background:rgba(179,38,30,.10);color:var(--alarm)}
  .kind{font-size:13.5px;color:var(--ink-2)}
  .card h3{font-size:21px;margin-bottom:10px}
  .card p{color:var(--ink-2)}
  .lbl{font-size:13.5px;font-weight:680;color:var(--ink);margin:18px 0 7px}
  .card ul{list-style:none;display:grid;gap:7px}
  .card li{color:var(--ink-2);font-size:16px;padding-left:18px;position:relative;word-break:break-word}
  .card li::before{content:"";position:absolute;left:0;top:.7em;width:6px;height:6px;border-radius:50%;background:var(--flag-fill)}
  .card.alert li::before{background:var(--alarm)}
  .action{color:var(--ink)!important}
  .clear{background:var(--bg-2);border:1px solid rgba(31,122,82,.35);border-left:3px solid var(--real);border-radius:16px;padding:28px;box-shadow:var(--shadow)}
  .clear h3{color:var(--real);font-size:21px;margin-bottom:10px}
  .clear p{color:var(--ink-2)}
  footer{border-top:1px solid var(--line);margin-top:36px;padding:30px 0 60px;color:var(--ink-2);font-size:15px}
  footer p{margin-bottom:10px;max-width:70ch}
  footer a{color:var(--flag)}
  @media print{body{background:#fff}.card,.clear{box-shadow:none}}
</style>
</head>
<body>
<header><div class="wrap">
  <div class="brand">
    <svg viewBox="0 0 32 32" fill="none"><path d="M16 3l11 4v9c0 7-4.7 11.6-11 13C9.7 27.6 5 23 5 16V7l11-4z" stroke="#F6A623" stroke-width="2" stroke-linejoin="round"/></svg>
    ImposterShield
  </div>
  <h1>$(Esc $headline)</h1>
  <p class="sub">$(Esc $subline)</p>
  <p class="meta">$(Esc $machine) &middot; $(Esc $stamp) &middot; scan v$ScanVersion</p>
</div></header>
<main><div class="wrap">
"@

if ($all.Count -eq 0) {
  $html += @"
  <div class="clear">
    <h3>Nothing found</h3>
    <p>Keep in mind what this scan does and does not do. It reads what is running and what starts with Windows, and matches it against software that can watch a screen or control a computer. It is not a virus scanner, and it cannot see a brand new tool nobody has catalogued yet. Run it again if something ever feels off.</p>
  </div>
"@
} else {
  $html += $cards.ToString()
}

$html += @"
</div></main>
<footer><div class="wrap">
  <p><strong>Nothing on this page left your computer.</strong> The scan made no network connections and sent nothing to us. This file is yours; delete it whenever you like.</p>
  <p>This is an early build. It is good at finding remote-control software, monitoring software and AI tools that are already known. It cannot yet judge whether a program is behaving badly, and it does not replace an antivirus.</p>
  <p>Something look wrong? Tell us at <a href="mailto:hello@impostershield.com">hello@impostershield.com</a> &middot; <a href="https://impostershield.com">impostershield.com</a></p>
</div></footer>
</body>
</html>
"@

if (-not $ReportPath) {
  $desktop = [Environment]::GetFolderPath('Desktop')
  if (-not $desktop -or -not (Test-Path $desktop)) { $desktop = $env:USERPROFILE }
  $ReportPath = Join-Path $desktop ("ImposterShield-Scan-{0}.html" -f (Get-Date -Format 'yyyy-MM-dd-HHmm'))
}
[IO.File]::WriteAllText($ReportPath, $html, (New-Object Text.UTF8Encoding $false))

if ($Json) {
  $payload = [pscustomobject]@{
    version   = $ScanVersion
    scannedAt = (Get-Date).ToString('o')
    computer  = $machine
    alerts    = $alerts.Count
    notes     = $notes.Count
    findings  = @($all | ForEach-Object {
      [pscustomobject]@{
        id = $_.id; name = $_.name; kind = $_.kind; level = $_.level
        what = $_.what; action = (Get-Action $_); evidence = @($_.evidence)
      }
    })
  }
  $payload | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $Json -Encoding UTF8
}

if (-not $Quiet) {
  Write-Host ''
  Write-Host '  Your report has been saved here:' -ForegroundColor Gray
  Write-Host "  $ReportPath" -ForegroundColor White
  Write-Host ''
}

# Opening the report is the last thing that happens and the least important. A machine with
# no browser association, or a locked-down one, would otherwise throw here and turn a scan
# that completed perfectly into an apparent failure.
if (-not $NoOpen) {
  try {
    Start-Process $ReportPath -ErrorAction Stop
  } catch {
    if (-not $Quiet) {
      Write-Host '  Could not open your web browser automatically.' -ForegroundColor DarkYellow
      Write-Host '  Open the file above yourself: it is saved on your Desktop.' -ForegroundColor DarkYellow
      Write-Host ''
    }
  }
}
