Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# ShellKnight Changelog

## [v2026.09.25.001] - 2026-09-25

- **Assessment Engine results now reach the report and the score (critical):** `Invoke-SafeBlock` runs its block with `& $Block`, which is a child scope. The engine set `$avProduct`, `$edrProduct`, `$defStatus`, `$bitlockerWarn`, `$osEolWarn` and `$wuLastWarn` without a `$Script:` prefix, so each assignment made a local copy that was discarded when the block returned. The payload and the scoring read the script-level defaults instead, and have done since v1.002. **Every device reported `antivirus: "NONE DETECTED"`, `edr: "None detected"` and `defender: "Unknown"`**, and the BitLocker, OS end-of-life and Windows Update penalties never applied. `MachineInfo` and the log had the real values throughout. The payload now reads `antivirus`, `edr` and `defender` from `MachineInfo`, like the other machine fields, so they are null when the engine did not run instead of a default reported as fact. The three warn flags are `$Script:`-scoped. The three script-level defaults are gone: those values are now local to the engine, and `$avProduct` is assigned on every branch.
- **Scoring change: BitLocker, OS end-of-life and Windows Update now count.** A device loses 15 points if BitLocker is off on C:, 20 if its build is past the end-of-life date in the engine's table, and 15 if the last Windows Update install was over 30 days ago, up to 50 in all. These rules have been in the script since v1.002 but never fired. Each flag is set only by a positive detection; a probe that fails or returns nothing leaves it off, so a collection failure cannot lower a grade (ADR 0009). **Expect grades to drop fleet-wide on the first run of this version.** That is a measurement correction; nothing changed on the endpoints.
- **Scoring change: the Defender DISABLED rule (-20) is removed.** It never fired either. Live, it would take 20 points from every box whose third-party AV has turned Defender off, which Windows does by design, and on a box with no AV at all it would stack with the -25 "no active AV" rule. That -25 rule already scores a missing AV, and scores it once.
- **Persistence Engine summary lines:** the same bug made "no malware Run keys found" and "no browser policy hijacks found" appear in the log even after a removal, because their counters were incremented inside an `Invoke-SafeBlock`. The counters are now `$Script:`-scoped. This affects log text only: `run_keys_removed`, `ioc_alerts` and the score were already right.
- **Regression test:** `tests/Test-EngineScope.ps1` runs the Phase 2 code, the security scoring and the payload's machine fields verbatim, under StrictMode 2 with mocked Windows cmdlets. It covers BitLocker off (both probes), an end-of-life build, a stale Windows Update, all three together, third-party AV with Defender off, an EDR, no AV at all, and an engine that aborts or is disabled. It also checks the whole script, via the AST, for a variable assigned bare inside an `Invoke-SafeBlock` and then read outside it, which is this bug in general form. It fails against v2026.09.24.001 and passes here.

## [v2026.09.24.001] - 2026-09-24

- **Check-ins restored: device identity no longer depends on the Assessment Engine (critical):** v2026.09.08.001 replaced the Defender catch that set `$defSigs = 'Unknown'` with fallbacks that set it only on success. Where every probe fails (`Get-MpComputerStatus` throws under SYSTEM, and `MSFT_MpComputerStatus` is missing or has no signature date because a third-party AV owns the box or Defender has been removed), reading the unset `$defSigs` in the `MachineInfo` literal threw under `Set-StrictMode -Version 2`. `Invoke-SafeBlock` logged it and moved on, `MachineInfo` stayed empty, and **`device_id` was sent as null**. Battlefield fell back to `host:<name>`, which frozen enrollment does not recognise for a device enrolled by hardware UUID, so every POST was answered `200 {"status":"ignored"}` and nothing was stored. This is very likely the 2026-09-09 reporting drop that v2026.09.15.001 could not explain. `$defSigs`, and `$wuStr` (unset on an empty Windows Update history), now start as `'Unknown'`. Device identity (hardware UUID, then MachineGuid, then `host:<name>`; same values as before) is now computed in its own block ahead of the engine and regardless of `AssessmentEngine_Enabled`. The result, `$Script:DeviceId`, starts at the hostname fallback so it is never null, and both `MachineInfo['Device ID']` and the payload `device_id` read it. An engine failure now costs machine details, never the check-in.
Expand Down
112 changes: 76 additions & 36 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.24.001 - Enterprise Endpoint Security & Remediation Tool
ShellKnight v2026.09.25.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.24.001
Released : 2026-09-24
Prior : v2026.09.15.001
Version : v2026.09.25.001
Released : 2026-09-25
Prior : v2026.09.24.001

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

.CHANGELOG
v2026.09.25.001 - Assessment Engine results now reach the report and the
score. Invoke-SafeBlock runs its block as a child scope
(& $Block). The engine set $avProduct, $edrProduct, $defStatus,
$bitlockerWarn, $osEolWarn and $wuLastWarn with bare
assignments, and each one made a local copy that was discarded
when the block returned. So the payload and the scoring saw the
script-level defaults on every run since v1.002: antivirus
'NONE DETECTED', edr 'None detected' and defender 'Unknown' on
every device, and the BitLocker (-15), OS EOL (-20) and Windows
Update (-15) penalties never applied. MachineInfo and the log
were right all along. The payload now reads antivirus, edr and
defender from MachineInfo, so when the engine did not run they
are null rather than a default reported as fact. The three flags
are $Script:-scoped.
SCORING CHANGE. Devices with BitLocker off, an end-of-life build,
or no Windows Update install in over 30 days lose 15, 20 and 15
points respectively, up to 50 in all. Each flag is set only by a
positive detection, so a probe that fails costs nothing. The
Defender DISABLED rule (-20) is removed rather than switched on.
Live, it would hit every box whose third-party AV turns Defender
off, and on a box with no AV it would stack with the -25 no-AV
rule. Expect grades to drop on the first run of this version.
That is a measurement correction; nothing changed on the
endpoints.
The same bug made the Persistence Engine log "no malware Run keys
found" and "no browser policy hijacks found" even after a
removal. Those counters are $Script:-scoped too (log text only).
v2026.09.24.001 - Check-ins restored; two silent field failures fixed.
(1) Devices "ignored" by Battlefield. v2026.09.08.001 dropped the
Defender catch that set $defSigs = 'Unknown', so where every probe
Expand Down Expand Up @@ -431,7 +458,7 @@


# ==============================================================================
# SHELLKNIGHT v2026.09.24.001 CONFIGURATION
# SHELLKNIGHT v2026.09.25.001 CONFIGURATION
# All settings are configured here. No external config files required.
# Each engine can be independently enabled or disabled.
# ==============================================================================
Expand Down Expand Up @@ -591,7 +618,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 621 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 621 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 @@ -620,11 +647,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 650 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 650 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.24.001'
Version = 'v2026.09.25.001'
# Intel Engine
IntelEngine_Enabled = $SK_IntelEngine_Enabled
IntelEngine_CheckUpdates = $SK_IntelEngine_CheckForUpdates
Expand Down Expand Up @@ -907,7 +934,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 937 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 937 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 @@ -966,7 +993,7 @@
}
}
}
} catch { } # denied dir: skip it, continue with the rest of the stack

Check warning on line 996 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 996 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 @@ -983,7 +1010,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 1013 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 1013 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 @@ -1014,7 +1041,7 @@

# Banner
$bannerWidth = 78
$version = 'ShellKnight v2026.09.24.001'
$version = 'ShellKnight v2026.09.25.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 @@ -1089,7 +1116,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 1119 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 1119 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 @@ -1208,12 +1235,15 @@
Log-Info '--- Phase 2: Assessment Engine ---'

$Script:MachineInfo = [ordered]@{}
$bitlockerWarn = $false
$osEolWarn = $false
$wuLastWarn = $false
$avProduct = 'NONE DETECTED'
$edrProduct = 'None detected'
$defStatus = 'Unknown'
# Read by the scoring after the engine, so the engine sets them with $Script:.
# Invoke-SafeBlock runs the engine as a child scope (& $Block), where a bare
# assignment makes a local that is gone when the block returns. Until
# v2026.09.25.001 these, and the payload's antivirus/edr/defender, were bare
# script-level variables, so the scoring and the report only ever saw their
# defaults. The payload now reads those three from MachineInfo.
$Script:BitLockerWarn = $false
$Script:OsEolWarn = $false
$Script:WuLastWarn = $false
# Scored separately from $avProduct so a *failed* detection can never be scored
# as "unprotected" - that mistake has cost the whole fleet 25 points twice now
# (v2026.07.30.001's aborted engine, and the Defender-excluded-from-AV bug).
Expand Down Expand Up @@ -1244,7 +1274,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 1277 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 1277 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 @@ -1286,13 +1316,13 @@
try {
$bl = Get-BitLockerVolume -MountPoint 'C:' -ErrorAction Stop
$blStatus = $bl.ProtectionStatus
if ($blStatus -ne 'On') { $bitlockerWarn = $true; $blStatus = 'Off' } else { $blStatus = 'On' }
if ($blStatus -ne 'On') { $Script:BitLockerWarn = $true; $blStatus = 'Off' } else { $blStatus = 'On' }
} catch {
try {
$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'; $bitlockerWarn = $true }
$blStatus = if ($blWmi.ProtectionStatus -eq 1) { 'On' } else { 'Off'; $Script:BitLockerWarn = $true }
} catch { }

Check warning on line 1325 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 1325 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
Expand All @@ -1311,7 +1341,7 @@
}
$eolDate = $eolDates[$osBuild]
$eolStr = if ($eolDate) {
if ((Get-Date) -gt $eolDate) { $osEolWarn = $true; "END OF LIFE (since $($eolDate.ToString('yyyy-MM-dd')))"}
if ((Get-Date) -gt $eolDate) { $Script:OsEolWarn = $true; "END OF LIFE (since $($eolDate.ToString('yyyy-MM-dd')))"}
else { "Supported until $($eolDate.ToString('yyyy-MM-dd'))" }
} else { 'Unknown' }

Expand All @@ -1331,7 +1361,7 @@
if ($avName -match 'Windows Defender|Microsoft Defender') { $defenderRegistered = $true }
else { $avProducts.Add($avName) }
}
} catch { }

Check warning on line 1364 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 1364 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 @@ -1403,7 +1433,7 @@
try {
$disableRtp = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows Defender\Real-Time Protection' `
-Name 'DisableRealtimeMonitoring' -ErrorAction Stop).DisableRealtimeMonitoring
} catch { }

Check warning on line 1436 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 1436 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 All @@ -1413,9 +1443,12 @@
# Resolve the reported AV product and whether the box is actually
# protected. Defender registered with Security Center counts as
# protection unless we positively know real-time protection is off.
# Every branch assigns: $avProduct is local to the engine now, so an
# unassigned one would throw in the MachineInfo literal (StrictMode).
if ($avProducts.Count -gt 0) { $avProduct = $avProducts -join ', ' }
elseif ($defStatus -eq 'Active') { $avProduct = 'Windows Defender' }
elseif ($defenderRegistered) { $avProduct = "Windows Defender (status $defStatus)" }
else { $avProduct = 'NONE DETECTED' }
$Script:HasActiveAv = ($avProducts.Count -gt 0) -or ($defStatus -eq 'Active') -or
($defenderRegistered -and $defStatus -ne 'DISABLED')
$Script:AvDetectionRan = $true
Expand All @@ -1432,7 +1465,7 @@
$wuDate = $history.Item(0).Date
$wuDaysAgo = ([datetime]::Now - $wuDate).Days
$wuStr = "$($wuDate.ToString('yyyy-MM-dd')) ($wuDaysAgo days ago)"
if ($wuDaysAgo -gt 30) { $wuLastWarn = $true }
if ($wuDaysAgo -gt 30) { $Script:WuLastWarn = $true }
}
} catch { $wuStr = 'Unknown' }

Expand Down Expand Up @@ -1496,13 +1529,13 @@

# Screen summary
Write-Host " Hostname: $($env:COMPUTERNAME) | OS: $osName | RAM: $ramGB GB | Disk: $diskFreeGB GB free" -ForegroundColor White
if ($osEolWarn) { Log-Warn "OS EOL: $eolStr" }
if ($bitlockerWarn){
if ($Script:OsEolWarn) { Log-Warn "OS EOL: $eolStr" }
if ($Script:BitLockerWarn) {
Log-Warn "BitLocker: C: drive is NOT encrypted"
Add-Finding -Severity Medium -Title 'BitLocker not enabled on C:' -Action 'Enable BitLocker (required for HIPAA/CJIS clients; escrow recovery key in AD/RMM)'
}
if ($pcAgeYrs -gt 5){ Log-Warn "Aging hardware: PC is $pcAgeYrs years (BIOS: $($biosDate.ToString('yyyy-MM-dd')))" }
if ($wuLastWarn) { Log-Warn "Windows Update: last install was $wuDaysAgo days ago" }
if ($Script:WuLastWarn) { Log-Warn "Windows Update: last install was $wuDaysAgo days ago" }

# Hyper-V detection
Invoke-SafeBlock -Label 'Hyper-V detection' -Block {
Expand Down Expand Up @@ -1928,7 +1961,7 @@
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run'
)

$runKeysRemoved = 0
$Script:RunKeysFound = 0 # $Script: - incremented inside Invoke-SafeBlock (child scope)
foreach ($keyPath in $runKeyPaths) {
if (-not (Test-Path $keyPath)) { continue }
Invoke-SafeBlock -Label "Run key $keyPath" -Block {
Expand All @@ -1944,14 +1977,14 @@
Log-Success "Removed Run key: $name"
$Script:Counters.RunKeysRemoved++
$Script:Counters.IOCsFound++
$runKeysRemoved++
$Script:RunKeysFound++
} else {
Log-Info " [RUN] $name = $val"
}
}
}
}
if ($runKeysRemoved -eq 0) { Log-Summary "Persistence Engine - no malware Run keys found" }
if ($Script:RunKeysFound -eq 0) { Log-Summary "Persistence Engine - no malware Run keys found" }

# Per-user Run / RunOnce keys via HKEY_USERS.
# Running as SYSTEM, HKCU above is SYSTEM's own hive - real users' Run keys
Expand Down Expand Up @@ -2054,7 +2087,7 @@
'HKCU:\SOFTWARE\Policies\Google\Chrome',
'HKCU:\SOFTWARE\Policies\Microsoft\Edge'
)
$policyRemoved = 0
$Script:PolicyRemoved = 0 # $Script: - incremented inside Invoke-SafeBlock (child scope)
foreach ($policyPath in $browserPolicyPaths) {
if (-not (Test-Path $policyPath)) { continue }
Invoke-SafeBlock -Label "Browser policy $policyPath" -Block {
Expand All @@ -2064,12 +2097,12 @@
Log-IOC "Suspicious browser policy: $($_.Name) = $($_.Value)"
Remove-ItemProperty -Path $policyPath -Name $_.Name -Force -ErrorAction SilentlyContinue
Log-Success "Removed browser policy key: $($_.Name)"
$policyRemoved++
$Script:PolicyRemoved++
$Script:Counters.IOCsFound++
}
}
}
if ($policyRemoved -eq 0) { Log-Summary "Persistence Engine - no browser policy hijacks found" }
if ($Script:PolicyRemoved -eq 0) { Log-Summary "Persistence Engine - no browser policy hijacks found" }

# Defender exclusion audit
Invoke-SafeBlock -Label 'Defender exclusions' -Block {
Expand Down Expand Up @@ -3268,10 +3301,17 @@
if ($Script:Counters.IOCsFound -gt 0) { $Script:SecurityScore -= [math]::Min(50, $Script:Counters.IOCsFound * 15) }
if ($Script:Counters.Failed) { $Script:SecurityScore -= 10 }
if ($Script:AvDetectionRan -and -not $Script:HasActiveAv) { $Script:SecurityScore -= 25 }
if ($defStatus -eq 'DISABLED') { $Script:SecurityScore -= 20 }
if ($osEolWarn) { $Script:SecurityScore -= 20 }
if ($bitlockerWarn) { $Script:SecurityScore -= 15 }
if ($wuLastWarn) { $Script:SecurityScore -= 15 }
# Defender DISABLED is not scored on its own. That rule (-20) was here from
# v1.002 but never fired, because it read a script-level $defStatus the engine
# never wrote to (see Phase 2). If it were live it would take 20 from every box
# whose third-party AV has turned Defender off, which Windows does by design.
# On a box with no AV at all it would stack on the -25 above. "No working AV"
# is scored once, above.
# These three are live from v2026.09.25.001. Each is set only by a positive
# detection; a probe that fails leaves it $false.
if ($Script:OsEolWarn) { $Script:SecurityScore -= 20 }
if ($Script:BitLockerWarn) { $Script:SecurityScore -= 15 }
if ($Script:WuLastWarn) { $Script:SecurityScore -= 15 }
if ($inactiveAccounts.Count -gt 0) { $Script:SecurityScore -= [math]::Min(15, $inactiveAccounts.Count * 5) }
try { $smb1Sc = Get-SmbServerConfiguration -ErrorAction Stop | Select-Object -ExpandProperty EnableSMB1Protocol
if ($smb1Sc) { $Script:SecurityScore -= 20 } } catch { }
Expand Down Expand Up @@ -3328,7 +3368,7 @@
$sepLine = '=' * 80

Log-Info $sepLine
Log-Info " ShellKnight v2026.09.24.001 - Report"
Log-Info " ShellKnight v2026.09.25.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 @@ -3341,7 +3381,7 @@
$bannerWidth2 = 78
Write-Host ''
Write-Host " $sepLine" -ForegroundColor Cyan
Write-Host " ShellKnight v2026.09.24.001 - Report" -ForegroundColor Cyan
Write-Host " ShellKnight v2026.09.25.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 @@ -3613,7 +3653,7 @@
$jsonPath = "$jsonDir\ShellKnight_${jsonStamp}_$($env:COMPUTERNAME).json"

$jsonData = [ordered]@{
version = 'v2026.09.24.001'
version = 'v2026.09.25.001'
device_id = $Script:DeviceId
hardware_type = $Script:MachineInfo['Hardware Type']
site_name = $SK_SiteName
Expand All @@ -3633,9 +3673,9 @@
disk_free_gb = $freeGB
disk_free_after = $freeAfterGB
bitlocker = $Script:MachineInfo['BitLocker']
antivirus = $avProduct
edr = $edrProduct
defender = $defStatus
antivirus = $Script:MachineInfo['Antivirus']
edr = $Script:MachineInfo['EDR']
defender = $Script:MachineInfo['Defender']
defender_sigs = $Script:MachineInfo['Defender Sigs']
last_wu_install = $Script:MachineInfo['Last WU Install']
domain = $Script:MachineInfo['Domain/Workgroup']
Expand Down
Loading
Loading