diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c5841a..e313a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # ShellKnight Changelog +## [v2026.10.13.001] - 2026-10-13 + +- **Windows Admin Center's tasks on Azure VMs are no longer removed:** the Process Engine removes any scheduled task whose command line matches its obfuscation pattern (`encodedcommand|...|hidden|noprofile|...`) unless the command is in `LegitTaskPaths`. Windows Admin Center's Azure VM extension runs its `WindowsAdminCenterHeartbeatCheck` and `WindowsAdminCenterTroubleshootTask` tasks as `Powershell.exe -WindowStyle Hidden -File "C:\Packages\Plugins\Microsoft.AdminCenter.AdminCenter\\Sme.VmExtension\..."`, so the first run on an Azure Virtual Desktop session host deleted both and reported them as High IOCs. Commands run from `\Packages\Plugins\Microsoft.AdminCenter.AdminCenter\` are now in `LegitTaskPaths`. The folder belongs to the Azure guest agent and is writable only by administrators. The same flags from any other folder, including other extensions under `C:\Packages\Plugins\`, are still removed. Restoring the deleted tasks means repairing the Windows Admin Center extension from the Azure portal. +- **Regression test:** new `tests/Test-LegitTasks.ps1` takes `LegitTaskPaths` and the obfuscation pattern from the script and checks that the real Windows Admin Center command lines are kept, that a later extension version is kept, and that hidden or encoded PowerShell anywhere else is still removed. It fails against v2026.09.25.003 and passes here. + ## [v2026.09.25.003] - 2026-09-25 - **OS end of life is Microsoft's date for the build and the edition:** the Assessment Engine looked up `os_eol` by build number only, with one date per build, and several dates were years past Microsoft's. 19045 (Windows 10 22H2) read 2030-10-14 for 2025-10-14; 22621 and 22631 (Windows 11 22H2 and 23H2) read 2027-10-12 and 2028-10-10, later than even their Enterprise dates; 26100 read 2029-10-14. One date per build also cannot be right: Home/Pro and Enterprise/Education reach end of servicing on different days, and 14393, 17763, 19044 and 26100 are also LTSB/LTSC releases or Windows Server 2016/2019/2025, which run for years longer. The new `Get-OsEolDate` takes the edition family from `Win32_OperatingSystem.Caption` (Home/Pro, Enterprise/Education, LTSB/LTSC, IoT Enterprise LTSC, Server) and holds every date from Microsoft Learn's release-health and lifecycle pages. A caption it cannot place, such as a localized one, gets a date only when that date holds for every edition the machine could be; otherwise `os_eol` is `Unknown`, which is not scored (ADR 0009). New builds: 25398 (Server 23H2), 26200 (Windows 11 25H2) and 28000 (Windows 11 26H1). `os_eol` keeps its three forms, so Battlefield needs no change. diff --git a/ShellKnight.ps1 b/ShellKnight.ps1 index b06386f..ef5b95d 100644 --- a/ShellKnight.ps1 +++ b/ShellKnight.ps1 @@ -2,7 +2,7 @@ #Requires -RunAsAdministrator <# .SYNOPSIS - ShellKnight v2026.09.25.003 - Enterprise Endpoint Security & Remediation Tool + ShellKnight v2026.10.13.001 - Enterprise Endpoint Security & Remediation Tool .DESCRIPTION Automated endpoint security remediation, threat detection, hardening, and @@ -18,9 +18,9 @@ C. David Burgess - PTech LLC .VERSION - Version : v2026.09.25.003 - Released : 2026-09-25 - Prior : v2026.09.25.002 + Version : v2026.10.13.001 + Released : 2026-10-13 + Prior : v2026.09.25.003 .ENGINES Phase 1 - Intel Engine : Threat intelligence download and cache @@ -33,6 +33,15 @@ Phase 8 - Reporting Engine : Reporting, trending, and extended checks .CHANGELOG + v2026.10.13.001 - Windows Admin Center's tasks on Azure VMs are no longer + removed. They run PowerShell with -WindowStyle Hidden from the + Azure extension folder, which the scheduled-task check matched as + obfuscation, so the Process Engine deleted + WindowsAdminCenterHeartbeatCheck and + WindowsAdminCenterTroubleshootTask on its first run. Commands run + from C:\Packages\Plugins\Microsoft.AdminCenter.AdminCenter\ are + now in LegitTaskPaths. + v2026.09.25.003 - OS end of life is Microsoft's date for the build AND the edition. The engine looked it up by build number only, one date per build, and several were years late: 19045 (Windows 10 22H2) @@ -497,7 +506,7 @@ param() # ============================================================================== -# SHELLKNIGHT v2026.09.25.003 CONFIGURATION +# SHELLKNIGHT v2026.10.13.001 CONFIGURATION # All settings are configured here. No external config files required. # Each engine can be independently enabled or disabled. # ============================================================================== @@ -690,7 +699,7 @@ try { # Runtime Config Object - single source of truth for all engines $Script:Config = [PSCustomObject]@{ - Version = 'v2026.09.25.003' + Version = 'v2026.10.13.001' # Intel Engine IntelEngine_Enabled = $SK_IntelEngine_Enabled IntelEngine_CheckUpdates = $SK_IntelEngine_CheckForUpdates @@ -800,7 +809,11 @@ $Script:LegitProcessNames = (New-Object 'System.Collections.Generic.HashSet[stri $Script:LegitTaskPaths = @( '*AppData\Roaming\Zoom\bin\*','*Teams\*','*Slack\*','*Spotify\*', - '*Discord\*','*Cricut*','*AppData\Roaming\PCDr\*','*BundleApplicationRepairTool.exe*' + '*Discord\*','*Cricut*','*AppData\Roaming\PCDr\*','*BundleApplicationRepairTool.exe*', + # Windows Admin Center's Azure VM extension. Its heartbeat and troubleshoot + # tasks run PowerShell -WindowStyle Hidden, which the obfuscation check + # matches. The folder is the Azure guest agent's, writable only by admins. + '*\Packages\Plugins\Microsoft.AdminCenter.AdminCenter\*' ) # Task Scheduler path prefixes for known-legitimate Microsoft system tasks. @@ -1144,7 +1157,7 @@ $Script:UseNewPSFeatures = $Script:PSVer -ge 5 # Banner $bannerWidth = 78 -$version = 'ShellKnight v2026.09.25.003' +$version = 'ShellKnight v2026.10.13.001' $hostname = $env:COMPUTERNAME $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $psver = "PS $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)" @@ -3472,7 +3485,7 @@ $freeAfterGB = if ($diskAfter) { [math]::Round($diskAfter.FreeSpace / 1GB, 1) } $sepLine = '=' * 80 Log-Info $sepLine -Log-Info " ShellKnight v2026.09.25.003 - Report" +Log-Info " ShellKnight v2026.10.13.001 - Report" Log-Info " Hostname : $($env:COMPUTERNAME)" Log-Info " Run Date : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Log-Info " Runtime : $runtime seconds" @@ -3485,7 +3498,7 @@ Log-Info $sepLine $bannerWidth2 = 78 Write-Host '' Write-Host " $sepLine" -ForegroundColor Cyan -Write-Host " ShellKnight v2026.09.25.003 - Report" -ForegroundColor Cyan +Write-Host " ShellKnight v2026.10.13.001 - Report" -ForegroundColor Cyan Write-Host " Hostname : $($env:COMPUTERNAME)" -ForegroundColor White Write-Host " Run Date : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor White Write-Host " Runtime : $runtime seconds" -ForegroundColor White @@ -3757,7 +3770,7 @@ $jsonStamp= Get-Date -Format 'yyyy-MM-dd_HHmm' $jsonPath = "$jsonDir\ShellKnight_${jsonStamp}_$($env:COMPUTERNAME).json" $jsonData = [ordered]@{ - version = 'v2026.09.25.003' + version = 'v2026.10.13.001' device_id = $Script:DeviceId hardware_type = $Script:MachineInfo['Hardware Type'] site_name = $SK_SiteName diff --git a/tests/Test-LegitTasks.ps1 b/tests/Test-LegitTasks.ps1 new file mode 100644 index 0000000..247bd06 --- /dev/null +++ b/tests/Test-LegitTasks.ps1 @@ -0,0 +1,66 @@ +<# +.SYNOPSIS + Regression test for the scheduled-task allow-list. + +.DESCRIPTION + The Process Engine removes any scheduled task whose command line matches + its obfuscation pattern unless the command is in LegitTaskPaths. On an + Azure VM that removed Windows Admin Center's heartbeat and troubleshoot + tasks, which run PowerShell -WindowStyle Hidden from the Azure extension + folder. This checks those commands are allowed and that the allow-list + entry is narrow: the same flags from anywhere else are still caught. + + ShellKnight.ps1 executes on load, so the list and the pattern are + extracted textually rather than dot-sourced. +#> +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +$scriptPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'ShellKnight.ps1' +$source = Get-Content -LiteralPath $scriptPath -Raw + +$list = [regex]::Match($source, '(?ms)^\$Script:LegitTaskPaths = @\(.*?^\)') +if (-not $list.Success) { throw 'LegitTaskPaths not found in ShellKnight.ps1' } +Invoke-Expression $list.Value + +$pat = [regex]::Match($source, "(?m)^\s*'(encodedcommand\|[^']+)',\s*$") +if (-not $pat.Success) { throw 'Scheduled-task obfuscation pattern not found in ShellKnight.ps1' } +$obfusc = New-Object System.Text.RegularExpressions.Regex -ArgumentList @( + $pat.Groups[1].Value, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + +function Test-Removed([string]$CmdLine) { + $isLegit = $Script:LegitTaskPaths | Where-Object { $CmdLine -like $_ } + return [bool]($obfusc.IsMatch($CmdLine) -and -not $isLegit) +} + +$failures = 0 +function Assert-Removed { + param([string]$CmdLine, [bool]$Expected, [string]$Label) + $actual = Test-Removed $CmdLine + if ($actual -eq $Expected) { + Write-Host " PASS $Label" + } else { + Write-Host " FAIL $Label - removed=$actual, expected $Expected" -ForegroundColor Red + $script:failures++ + } +} + +# Real command lines from an Azure Virtual Desktop session host (2026-10-13). +$wac = 'C:\Packages\Plugins\Microsoft.AdminCenter.AdminCenter\0.76.0.0\Sme.VmExtension' +Assert-Removed "Powershell.exe -WindowStyle Hidden -File `"$wac\HeartbeatCheck.ps1`"" $false 'WAC heartbeat task is kept' +Assert-Removed "Powershell.exe -WindowStyle Hidden -File `"$wac\Troubleshoot.ps1`"" $false 'WAC troubleshoot task is kept' +Assert-Removed "powershell.exe -WindowStyle Hidden -File `"C:\Packages\Plugins\Microsoft.AdminCenter.AdminCenter\0.80.0.0\x.ps1`"" $false 'a later WAC version is kept' + +# The same flags from anywhere else are still caught. +Assert-Removed 'powershell.exe -WindowStyle Hidden -File "C:\Users\Public\x.ps1"' $true 'hidden PowerShell elsewhere is removed' +Assert-Removed 'powershell.exe -WindowStyle Hidden -File "C:\Packages\Plugins\Other.Vendor\x.ps1"' $true 'another extension folder is not allowed' +Assert-Removed 'powershell.exe -enc SQBFAFgA' $true 'encoded command is removed' +Assert-Removed 'C:\Windows\System32\cleanmgr.exe /autoclean' $false 'ordinary task is left alone' + +Write-Host '' +if ($failures -gt 0) { + Write-Host " FAILED - $failures assertion(s)" -ForegroundColor Red + exit 1 +} +Write-Host ' PASS - all assertions' -ForegroundColor Green +exit 0