<#
.SYNOPSIS
    Offline (WinPE / WinRE) neutralization of Sophos Endpoint tamper protection
    so that SophosZap can complete removal after a single reboot.

.DESCRIPTION
    Inherited-MSP Sophos installs with NO Sophos Central access cannot be removed
    from inside Windows: tamper protection is enforced by a boot-start kernel
    driver (SophosED.sys / SophosEL.sys), and SophosZap refuses to run while the
    registry flag SEDEnabled = 1.

    Run this from a PowerShell prompt in WinPE / WinRE (NOT normal Windows),
    pointed at the OFFLINE Windows volume. It performs every edit needed so that
    after ONE reboot, SophosZap --confirm runs cleanly:

        1. Renames Sophos*.sys driver files  ->  .old      (cannot load at boot)
        2. Sets the "Sophos Endpoint Defense" service  Start = 4 (Disabled)
        3. Clears the tamper flags  SEDEnabled = 0  and  IgnoreSAV = 0

    It asks for the Windows drive letter, proves the volume is really Windows
    (not the ~600 MB recovery partition), shows you the current values before
    changing anything, and confirms at every destructive step.

.NOTES
    Origin    : Built from the Lone Star Electrical LS-1 removal, 2026-06-02.
    Run from  : WinPE / WinRE -> Command Prompt -> powershell  (or a PE with PS).
    Requires  : the target Windows volume must be UNLOCKED. If BitLocker is on,
                System32\config\SYSTEM is unreadable -- unlock with the recovery
                key first (manage-bde -unlock X: -RecoveryPassword <key>), or
                confirm BitLocker OFF from normal Windows before booting to PE.

    AFTER this script:
        a. Remove the PE USB.
        b. Reboot into normal Windows.
        c. Run:  SophosZap.exe --confirm        (pass 1 -- bulk removal)
        d. Reboot when it says "reboot and re-execute".
        e. Run:  SophosZap.exe --confirm        (pass 2 -- finishes the job)
        f. Verify: no Sophos services, drivers, folders, or Add/Remove entries;
                   Windows Defender real-time protection ON.
#>

[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'
$HiveMount = 'HKLM\OFFSYS'   # temporary mount point for the offline SYSTEM hive

function Write-Head([string]$t) { Write-Host ""; Write-Host "==== $t ====" -ForegroundColor Cyan }
function Write-Ok  ([string]$t) { Write-Host "  [OK]    $t" -ForegroundColor Green }
function Write-Warn([string]$t) { Write-Host "  [WARN]  $t" -ForegroundColor Yellow }
function Write-Err ([string]$t) { Write-Host "  [ERROR] $t" -ForegroundColor Red }

function Confirm-Step([string]$Message) {
    $ans = Read-Host "$Message  [y/N]"
    return ($ans.Trim() -match '^(y|yes)$')
}

Write-Host @"
============================================================
 Sophos Offline Removal (PE) - tamper-protection neutralizer
============================================================
 This edits an OFFLINE Windows volume. Make sure you are in
 WinPE/WinRE, NOT the live Windows you want to clean.
"@ -ForegroundColor White

try {

# ---------------------------------------------------------------------------
# 1. Identify and validate the Windows drive letter
# ---------------------------------------------------------------------------
Write-Head "Step 1 - Identify the offline Windows volume"
Write-Host "Volumes visible in this PE session:"
Get-Volume -ErrorAction SilentlyContinue |
    Where-Object DriveLetter |
    Select-Object DriveLetter, FileSystemLabel,
        @{n='Size(GB)';e={[math]::Round($_.Size/1GB,1)}},
        @{n='Free(GB)';e={[math]::Round($_.SizeRemaining/1GB,1)}} |
    Format-Table -AutoSize | Out-String | Write-Host

$drive = $null
do {
    $entry = (Read-Host "Enter the Windows drive letter as shown HERE in PE (e.g. C, D, E)").Trim().TrimEnd(':')
    if ($entry -notmatch '^[A-Za-z]$') { Write-Warn "Enter a single letter."; continue }
    $win  = "${entry}:\Windows"
    $hive = "${entry}:\Windows\System32\config\SYSTEM"
    if (-not (Test-Path $win))  { Write-Warn "$win not found -- that is not the Windows volume."; continue }
    if (-not (Test-Path $hive)) { Write-Warn "$hive not found -- volume locked by BitLocker? Unlock it first."; continue }
    $drive = $entry.ToUpper()
} while (-not $drive)

# Prove it is the real OS volume, not the recovery partition
Write-Host ""
Write-Host "Evidence that ${drive}: is the real Windows volume:"
foreach ($p in 'Windows','Windows\System32','Windows\System32\config','Users','Program Files') {
    $present = Test-Path "${drive}:\$p"
    "{0,-28} {1}" -f $p, $(if ($present) {'present'} else {'MISSING'}) | Write-Host
}
Write-Host ""
if (-not (Confirm-Step "Is ${drive}: definitely the Windows install you want to clean?")) {
    Write-Err "Aborted by user. No changes made."; return
}
$driversDir = "${drive}:\Windows\System32\drivers"
$systemHive = "${drive}:\Windows\System32\config\SYSTEM"

# ---------------------------------------------------------------------------
# 2. Find Sophos kernel driver files
# ---------------------------------------------------------------------------
Write-Head "Step 2 - Sophos driver files on disk"
$sophosDrivers = @(Get-ChildItem $driversDir -Filter 'Sophos*.sys' -ErrorAction SilentlyContinue)
if ($sophosDrivers.Count -eq 0) {
    Write-Warn "No Sophos*.sys driver files found (already removed, or different names)."
} else {
    $sophosDrivers | Select-Object Name, Length, LastWriteTime | Format-Table -AutoSize | Out-String | Write-Host
}
# Note: *.man files are ETW manifests, not drivers -- SophosZap removes them. Ignore here.

# ---------------------------------------------------------------------------
# 3. Load the offline SYSTEM hive and resolve the active ControlSet
# ---------------------------------------------------------------------------
Write-Head "Step 3 - Load the offline registry hive"
# Clean up a stale mount from a previous aborted run, if any.
reg unload $HiveMount 2>$null | Out-Null
$loaded = $false
try {
    & reg load $HiveMount $systemHive | Out-Null
    if ($LASTEXITCODE -ne 0) { throw "reg load failed (exit $LASTEXITCODE). Is the hive in use / volume locked?" }
    $loaded = $true
    Write-Ok "Loaded $systemHive as $HiveMount"

    # Offline hives have ControlSet001/002 + Select\Current -- NOT CurrentControlSet.
    $controlSet = 'ControlSet001'
    $sel = & reg query "$HiveMount\Select" /v Current 2>$null
    if ($sel -match 'Current\s+REG_DWORD\s+0x([0-9a-fA-F]+)') {
        $controlSet = "ControlSet{0:D3}" -f [Convert]::ToInt32($matches[1], 16)
    }
    Write-Ok "Active control set: $controlSet"

    $svcKey = "$HiveMount\$controlSet\Services\Sophos Endpoint Defense"
    $tpKey  = "$svcKey\TamperProtection\Config"

    # -----------------------------------------------------------------------
    # 4. Show current values BEFORE changing anything
    # -----------------------------------------------------------------------
    Write-Head "Step 4 - Current Sophos tamper state (offline hive)"
    $svcExists = $false
    & reg query $svcKey 2>$null | Out-Null
    if ($LASTEXITCODE -eq 0) {
        $svcExists = $true
        Write-Host "Service 'Sophos Endpoint Defense' -> Start:"
        & reg query $svcKey /v Start 2>$null | Where-Object { $_ -match 'Start' } | Write-Host
        Write-Host "TamperProtection flags:"
        & reg query $tpKey /v SEDEnabled 2>$null | Where-Object { $_ -match 'SEDEnabled' } | Write-Host
        & reg query $tpKey /v IgnoreSAV  2>$null | Where-Object { $_ -match 'IgnoreSAV'  } | Write-Host
    } else {
        Write-Warn "Service key 'Sophos Endpoint Defense' not found under $controlSet (already removed?)."
    }

    Write-Host ""
    Write-Host "Planned changes:" -ForegroundColor White
    Write-Host "  - rename $($sophosDrivers.Count) Sophos*.sys driver file(s) to .old"
    Write-Host "  - set service 'Sophos Endpoint Defense' Start = 4 (Disabled)"
    Write-Host "  - set SEDEnabled = 0  and  IgnoreSAV = 0"
    Write-Host ""
    if (-not (Confirm-Step "Apply these changes to ${drive}: now?")) {
        Write-Err "Aborted by user before changes. Unloading hive, no edits made."
        return
    }

    # -----------------------------------------------------------------------
    # 5. Apply registry edits (hive still loaded)
    # -----------------------------------------------------------------------
    Write-Head "Step 5 - Apply registry edits"
    if ($svcExists) {
        & reg add $svcKey /v Start /t REG_DWORD /d 4 /f | Out-Null
        if ($LASTEXITCODE -eq 0) { Write-Ok "Service Start set to 4 (Disabled)" } else { Write-Err "Failed to set Start" }

        & reg add $tpKey /v SEDEnabled /t REG_DWORD /d 0 /f | Out-Null
        if ($LASTEXITCODE -eq 0) { Write-Ok "SEDEnabled set to 0" } else { Write-Warn "Could not set SEDEnabled (key may not exist on this version)" }

        & reg add $tpKey /v IgnoreSAV /t REG_DWORD /d 0 /f | Out-Null
        if ($LASTEXITCODE -eq 0) { Write-Ok "IgnoreSAV set to 0" } else { Write-Warn "Could not set IgnoreSAV" }

        Write-Host ""
        Write-Host "Read-back after edit:"
        & reg query $svcKey /v Start      2>$null | Where-Object { $_ -match 'Start' }      | Write-Host
        & reg query $tpKey  /v SEDEnabled 2>$null | Where-Object { $_ -match 'SEDEnabled' } | Write-Host
    } else {
        Write-Warn "No SED service key to edit -- skipping registry changes."
    }
}
finally {
    if ($loaded) {
        [gc]::Collect(); Start-Sleep -Milliseconds 300
        & reg unload $HiveMount 2>$null | Out-Null
        if ($LASTEXITCODE -eq 0) { Write-Ok "Unloaded offline hive ($HiveMount)" }
        else { Write-Warn "reg unload reported a non-zero exit -- if it stayed mounted, close regedit/handles and run: reg unload $HiveMount" }
    }
}

# ---------------------------------------------------------------------------
# 6. Rename the driver files (after the hive is unloaded)
# ---------------------------------------------------------------------------
Write-Head "Step 6 - Rename Sophos driver files"
if ($sophosDrivers.Count -gt 0) {
    if (Confirm-Step "Rename $($sophosDrivers.Count) Sophos*.sys file(s) to .old so they cannot load?") {
        foreach ($f in $sophosDrivers) {
            $target = "$($f.FullName).old"
            try {
                if (Test-Path $target) { Remove-Item $target -Force }
                Rename-Item -LiteralPath $f.FullName -NewName "$($f.Name).old" -Force
                Write-Ok "Renamed $($f.Name) -> $($f.Name).old"
            } catch {
                Write-Err "Could not rename $($f.Name): $($_.Exception.Message)"
            }
        }
    } else {
        Write-Warn "Skipped driver rename (service Start=4 alone should still stop it loading)."
    }
} else {
    Write-Host "  (nothing to rename)"
}

# ---------------------------------------------------------------------------
# 7. Next steps
# ---------------------------------------------------------------------------
Write-Head "DONE - offline edits complete"
Write-Host @"
Next, in NORMAL Windows (not PE):

  1. Remove the PE USB so the box boots to Windows.
  2. Reboot into Windows.
  3. Run:   SophosZap.exe --confirm        (pass 1)
  4. Reboot when it reports 'reboot and re-execute'.
  5. Run:   SophosZap.exe --confirm        (pass 2)
  6. Verify clean:
       Get-Service *sophos*            -> nothing
       dir C:\Windows\System32\drivers\Sophos*   -> nothing (or only *.old)
       'C:\Program Files\Sophos','C:\ProgramData\Sophos'  -> gone
       Get-MpComputerStatus            -> RealTimeProtectionEnabled = True

If SophosZap still says 'tamper protection on', the SEDEnabled flag did not
clear -- re-check HKLM\SYSTEM\CurrentControlSet\services\Sophos Endpoint Defense\
TamperProtection\Config\SEDEnabled in live Windows and set it to 0.
"@ -ForegroundColor White

}
catch {
    Write-Host ""
    Write-Err "Script stopped on an error:"
    Write-Host "  $($_.Exception.Message)" -ForegroundColor Red
    if ($_.InvocationInfo) { Write-Host "  at line $($_.InvocationInfo.ScriptLineNumber): $($_.InvocationInfo.Line.Trim())" -ForegroundColor DarkGray }
    # Best-effort: make sure we never leave the offline hive mounted after a crash.
    reg unload $HiveMount 2>$null | Out-Null
}
finally {
    Write-Host ""
    [void](Read-Host "Press Enter to close this window")
}
