Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# ShellKnight Changelog

## [v2026.09.26.001] - 2026-09-26

- **Event 7045 allow-list: Claude, ChatGPT/Codex and Malwarebytes.** The first run at CustomerF (HOST-F1) scored F (0/100) on 10 IOCs. Eight of them were Event 7045 service installs by legitimate software that re-registers its services on every update: Claude's `cowork-svc` and OpenAI's Codex sandbox service (registered as "ChatGPT"), both Microsoft Store packages under `C:\Program Files\WindowsApps`, and three Malwarebytes kernel drivers (`mbam.sys`, `mwac.sys`, `mbae.sys`). Each IOC costs 15 points, capped at 50.
- **Allowed by what cannot be borrowed, never by the service name.** The Store apps are matched on the package folder, anchored at `X:\Program Files\WindowsApps\` and ending in the publisher ID (`__pzs8sxrjxfjjc` for Claude, `__2p2nqsd0c76g0` for OpenAI) that the package's signing certificate determines; only the system can write there. The Malwarebytes drivers install to a bare `system32\drivers` path with no vendor folder, so they are matched by service name *and* exact driver file in the real Windows driver directory, through a new `$knownGoodSvcDrivers` table. That is stronger than the name-only entries used for the Avira drivers. A service merely named "Claude" or "ChatGPT", another publisher's package, a folder called `WindowsApps` anywhere else, or `mbam.sys` under another service name or directory still raises the IOC.
- **Scoring change, upward only:** devices running these apps stop losing up to 50 points to them. No device loses points.
- **Regression test:** new `tests/Test-Svc7045Allowlist.ps1` runs the Event log IOC block verbatim with mocked events: CustomerF's eight events, the other path forms a driver event records, existing entries, and thirteen look-alikes that must still alert.

## [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.
Expand Down
69 changes: 58 additions & 11 deletions ShellKnight.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
ShellKnight v2026.09.25.003 - Enterprise Endpoint Security & Remediation Tool
ShellKnight v2026.09.26.001 - Enterprise Endpoint Security & Remediation Tool

.DESCRIPTION
Automated endpoint security remediation, threat detection, hardening, and
Expand All @@ -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.09.26.001
Released : 2026-09-26
Prior : v2026.09.25.003

.ENGINES
Phase 1 - Intel Engine : Threat intelligence download and cache
Expand All @@ -33,6 +33,23 @@
Phase 8 - Reporting Engine : Reporting, trending, and extended checks

.CHANGELOG
v2026.09.26.001 - Event 7045 allow-list: Claude, ChatGPT/Codex and
Malwarebytes. The first run at CustomerF (HOST-F1) scored
F (0/100) on 10 IOCs, 8 of them service installs by legitimate
software that re-registers its services on every update: Claude's
cowork-svc and OpenAI's Codex sandbox service (named "ChatGPT"),
both Microsoft Store packages, and three Malwarebytes kernel
drivers. Each IOC costs 15 points, capped at 50. Allowed by what
cannot be borrowed, never by the service name: the Store apps by
package folder, anchored at X:\Program Files\WindowsApps and ending
in the publisher ID that the signing certificate determines; the
Malwarebytes drivers by service name AND exact driver file in the
real system32\drivers directory (new $knownGoodSvcDrivers, stronger
than the name-only Avira entries). A service merely named "Claude",
another publisher's package, a WindowsApps folder elsewhere, or
mbam.sys under another name or directory still raises the IOC.
SCORING CHANGE, upward only: devices running these apps lose up to
50 fewer points.
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)
Expand Down Expand Up @@ -497,7 +514,7 @@


# ==============================================================================
# SHELLKNIGHT v2026.09.25.003 CONFIGURATION
# SHELLKNIGHT v2026.09.26.001 CONFIGURATION
# All settings are configured here. No external config files required.
# Each engine can be independently enabled or disabled.
# ==============================================================================
Expand Down Expand Up @@ -657,7 +674,7 @@
if ($cfg.ScheduleHours) { $SK_ScheduleHours = [int]$cfg.ScheduleHours }
if ($null -ne $cfg.SelfSchedule) { $SK_SelfSchedule = [bool]$cfg.SelfSchedule }
if ($cfg.SiteName) { $SK_SiteName = $cfg.SiteName }
} catch { }

Check warning on line 677 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 677 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
}

# Environment-variable overrides (bootstrap via Datto sets these; env wins)
Expand Down Expand Up @@ -686,11 +703,11 @@
# back to the hardcoded IOC list (review finding 6b; field hit 2026-07-03).
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
} catch { }

Check warning on line 706 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 706 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

# Runtime Config Object - single source of truth for all engines
$Script:Config = [PSCustomObject]@{
Version = 'v2026.09.25.003'
Version = 'v2026.09.26.001'
# Intel Engine
IntelEngine_Enabled = $SK_IntelEngine_Enabled
IntelEngine_CheckUpdates = $SK_IntelEngine_CheckForUpdates
Expand Down Expand Up @@ -973,7 +990,7 @@
# ParseExact rejects against 'yyyyMMdd', so the legacy path was broken too.)
$s = [string]$ReleaseDate
if ($s.Length -ge 8) {
try { return [datetime]::ParseExact($s.Substring(0, 8), 'yyyyMMdd', $null) } catch { }

Check warning on line 993 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 993 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
}
return (Get-Date) # unknown age; scores treat this as a new machine
}
Expand Down Expand Up @@ -1096,7 +1113,7 @@
}
}
}
} catch { } # denied dir: skip it, continue with the rest of the stack

Check warning on line 1116 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1116 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
}
$sizes[$profileName] = $total
}
Expand All @@ -1113,7 +1130,7 @@
$beforeBytes = ($before | Measure-Object -Property Length -Sum).Sum
$removed = 0
foreach ($f in $before) {
try { Remove-Item -LiteralPath $f.FullName -Force -ErrorAction Stop; $removed++ } catch { }

Check warning on line 1133 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1133 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
}
if ($removed -gt 0) {
$freedMB = [math]::Round($beforeBytes / 1MB, 1)
Expand Down Expand Up @@ -1144,7 +1161,7 @@

# Banner
$bannerWidth = 78
$version = 'ShellKnight v2026.09.25.003'
$version = 'ShellKnight v2026.09.26.001'
$hostname = $env:COMPUTERNAME
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$psver = "PS $($PSVersionTable.PSVersion.Major).$($PSVersionTable.PSVersion.Minor)"
Expand Down Expand Up @@ -1219,7 +1236,7 @@
& schtasks.exe /Create /TN 'ShellKnight' /TR $action /SC HOURLY /MO $SK_ScheduleHours `
/ST $startTime /RU 'SYSTEM' /RL HIGHEST /F 2>$null | Out-Null
$Script:Health.task_ensured = ($LASTEXITCODE -eq 0)
try { $Script:Health.next_run = (Get-ScheduledTaskInfo -TaskName 'ShellKnight' -ErrorAction Stop).NextRunTime.ToString('o') } catch {}

Check warning on line 1239 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1239 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
Log-Info "Self-schedule ensured: every $SK_ScheduleHours h at :$startTime (SYSTEM)"
} catch { Log-Warn "Self-schedule failed: $($_.Exception.Message)" }
}
Expand Down Expand Up @@ -1382,7 +1399,7 @@
try { $null = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop } catch { $wmiUp = $false }
}
if (-not $deviceId -and $wmiUp) {
try { $deviceId = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid } catch { }

Check warning on line 1402 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1402 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
}
if ($deviceId) { $Script:DeviceId = $deviceId }
}
Expand Down Expand Up @@ -1430,7 +1447,7 @@
$blWmi = Get-CimInstance -Namespace 'Root\CIMV2\Security\MicrosoftVolumeEncryption' `
-ClassName 'Win32_EncryptableVolume' -Filter "DriveLetter='C:'" -ErrorAction Stop
$blStatus = if ($blWmi.ProtectionStatus -eq 1) { 'On' } else { 'Off'; $Script:BitLockerWarn = $true }
} catch { }

Check warning on line 1450 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1450 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
}

# OS EOL check: Microsoft's date for this build and edition
Expand All @@ -1456,7 +1473,7 @@
if ($avName -match 'Windows Defender|Microsoft Defender') { $defenderRegistered = $true }
else { $avProducts.Add($avName) }
}
} catch { }

Check warning on line 1476 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1476 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

# Datto AV (registered AV product; RMM handled elsewhere)
if (Get-Service -Name 'EndpointProtectionService2' -ErrorAction SilentlyContinue) {
Expand Down Expand Up @@ -1528,7 +1545,7 @@
try {
$disableRtp = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows Defender\Real-Time Protection' `
-Name 'DisableRealtimeMonitoring' -ErrorAction Stop).DisableRealtimeMonitoring
} catch { }

Check warning on line 1548 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

Check warning on line 1548 in ShellKnight.ps1

View workflow job for this annotation

GitHub Actions / validate

[PSAvoidUsingEmptyCatchBlock] Empty catch block is used. Please use Write-Error or throw statements in catch blocks.
$defRtp = ($wd.Status -eq 'Running' -and $disableRtp -ne 1)
}
}
Expand Down Expand Up @@ -3067,15 +3084,45 @@
'windows defender',
'drivers\\wd\\', # Defender's driver directory (KslD.sys, WdAiNisDrv.sys)
'dell\\saremediation', # Dell factory remediation plugin (BioNTDrv) - field FP 2026-09-08 CUSTA
'datto rollback driver' # Our own RMM rollback driver - field FP 2026-09-08 CUSTA
'datto rollback driver', # Our own RMM rollback driver - field FP 2026-09-08 CUSTA
# Microsoft Store (MSIX) apps that register a service on every update:
# Claude's cowork-svc and OpenAI's Codex sandbox service (the latter
# named "ChatGPT") - field FP 2026-09-26 CustomerF. Keyed on the package
# folder, never the service name: WindowsApps is writable only by the
# system, and the folder ends in the publisher ID that the package's
# signing certificate determines, so another publisher's package, or a
# service merely named "Claude", does not match. Anchored at the start
# (after the event's leading quote) so a folder named WindowsApps
# somewhere else does not match either.
'^"?[a-z]:\\program files\\windowsapps\\claude_[^\\"]*__pzs8sxrjxfjjc\\',
'^"?[a-z]:\\program files\\windowsapps\\openai\.[^\\"]*__2p2nqsd0c76g0\\'
) + @($Script:Config.Svc7045_ExtraPaths | Where-Object { $_ })

# Drivers that install to a bare system32\DRIVERS path, so there is no
# vendor directory to key on. Allowed only when the name AND the exact
# driver file both match, which is stronger than a name alone (the Avira
# entries above): a service called MBAMProtection anywhere else still
# alerts. Malwarebytes re-registers these on engine updates - field FP
# 2026-09-26 CustomerF.
# Service name -> driver file. Kernel-driver events record the path as
# C:\WINDOWS\system32\drivers\x.sys, \SystemRoot\System32\drivers\x.sys,
# \??\C:\..., or a bare System32\drivers\x.sys; $driverDir accepts those
# and nothing else, so a copy under C:\evil\system32\drivers\ still alerts.
$knownGoodSvcDrivers = @{
'MBAMProtection' = 'mbam.sys'
'MBAMWebProtection' = 'mwac.sys'
'Malwarebytes Anti-Exploit' = 'mbae.sys'
}
$driverDir = '^(\\\?\?\\)?([a-z]:\\windows\\|\\systemroot\\)?system32\\drivers\\'

$svcGroups = @{}
foreach ($evt in $svcEvents) {
$svcName = $evt.Properties[0].Value
$svcPath = $evt.Properties[1].Value
$svcAcct = $evt.Properties[4].Value
if ($knownGoodSvcs.Contains($svcName)) { continue }
if ($knownGoodSvcDrivers.ContainsKey($svcName) -and
"$svcPath".Trim().Trim('"') -match ($driverDir + [regex]::Escape($knownGoodSvcDrivers[$svcName]) + '$')) { continue }
# Path-based whitelist - skip events from known-good vendor install paths
$isKnownGoodPath = $knownGoodSvcPaths | Where-Object { $svcPath -match $_ }
if ($isKnownGoodPath) { continue }
Expand Down Expand Up @@ -3472,7 +3519,7 @@
$sepLine = '=' * 80

Log-Info $sepLine
Log-Info " ShellKnight v2026.09.25.003 - Report"
Log-Info " ShellKnight v2026.09.26.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"
Expand All @@ -3485,7 +3532,7 @@
$bannerWidth2 = 78
Write-Host ''
Write-Host " $sepLine" -ForegroundColor Cyan
Write-Host " ShellKnight v2026.09.25.003 - Report" -ForegroundColor Cyan
Write-Host " ShellKnight v2026.09.26.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
Expand Down Expand Up @@ -3757,7 +3804,7 @@
$jsonPath = "$jsonDir\ShellKnight_${jsonStamp}_$($env:COMPUTERNAME).json"

$jsonData = [ordered]@{
version = 'v2026.09.25.003'
version = 'v2026.09.26.001'
device_id = $Script:DeviceId
hardware_type = $Script:MachineInfo['Hardware Type']
site_name = $SK_SiteName
Expand Down
Loading
Loading