diff --git a/.claude/scripts/onboarding-diagnostic.ps1 b/.claude/scripts/onboarding-diagnostic.ps1 index f0d4aa9..eea7c23 100644 --- a/.claude/scripts/onboarding-diagnostic.ps1 +++ b/.claude/scripts/onboarding-diagnostic.ps1 @@ -164,6 +164,66 @@ Invoke-Check -Id 'sec.defender' -Category 'security' -Title 'Microsoft Defender $tamper = $false try { $tamper = [bool]$mp.IsTamperProtected } catch { $tamper = $false } + # Detect a managed/known 3rd-party AV. When one is active, Windows + # intentionally disables Defender real-time protection and stops its AM + # service (Defender steps aside for the registered AV). In that case + # "Defender RTP off" / "AM service not running" is EXPECTED, not a critical + # gap, so we downgrade those two findings to INFO below. + # + # Primary signal: a non-Defender SecurityCenter2 AntiVirusProduct whose + # productState reports real-time protection ENABLED. Fallback signal (Server + # SKUs, where SecurityCenter2 is absent): a running Datto AV/EDR service. + # Both are independent, cheap, idempotent CIM queries scoped to this check. + $thirdPartyAv = $false + $thirdPartyAvEvidence = '' + try { + $scAv = Get-CimInstance -Namespace 'root\SecurityCenter2' -ClassName AntiVirusProduct -ErrorAction Stop + foreach ($p in $scAv) { + $pn = [string]$p.displayName + # productState encodes the product's live status. Bit 0x1000 (the + # RTP byte's 0x10) set => real-time protection is ON. A registered + # but disabled / expired / snoozed AV (e.g. a lapsed OEM trial) + # leaves an AntiVirusProduct entry behind but is NOT protecting the + # endpoint, so it must not suppress the critical Defender finding. + # Require RTP-enabled before treating it as an active 3rd-party AV. + $state = 0 + try { $state = [int]$p.productState } catch { $state = 0 } + $rtpOn = (($state -band 0x1000) -ne 0) + if ($pn -and $rtpOn -and $pn -notmatch 'Windows Defender' -and $pn -notmatch 'Microsoft Defender') { + $thirdPartyAv = $true + $thirdPartyAvEvidence = "SecurityCenter2 AntiVirusProduct: $pn (productState=0x$('{0:X}' -f $state), RTP on)" + } + } + } catch { + # SecurityCenter2 unavailable (e.g. Server SKU). Fall through to service probe. + } + if (-not $thirdPartyAv) { + # Datto AV / Datto EDR service fallback. A bare "datto" substring is too + # loose: Datto RMM, Backup, Workplace, Continuity and File Protection all + # carry "datto" in their service/display names but provide NO antivirus, + # and would wrongly suppress the critical Defender finding. Require a + # Datto name AND an AV/EDR token, and explicitly exclude the non-AV Datto + # product lines. False negatives here are safe-side (critical preserved). + try { + $dattoSvc = Get-CimInstance -ClassName Win32_Service -ErrorAction Stop | + Where-Object { + $n = [string]$_.Name + $d = [string]$_.DisplayName + ((($n -match 'datto') -and ($n -match 'edr|antivirus|av')) -or + (($d -match 'datto') -and ($d -match 'edr|antivirus|av'))) -and + ($n -notmatch 'rmm|backup|workplace|continuity|file|networking') -and + ($d -notmatch 'rmm|backup|workplace|continuity|file|networking') + } | + Where-Object { [string]$_.State -eq 'Running' } | + Select-Object -First 1 + if ($dattoSvc) { + $thirdPartyAv = $true + $thirdPartyAvEvidence = "Active Datto AV/EDR service: $([string]$dattoSvc.Name) ($([string]$dattoSvc.DisplayName)) Running" + } + } catch { } + } + Set-Fact 'third_party_av_active' $thirdPartyAv + Set-Fact 'defender' @{ available = $true real_time_protection = $rtp @@ -177,16 +237,33 @@ Invoke-Check -Id 'sec.defender' -Category 'security' -Title 'Microsoft Defender $ev = "RealTimeProtectionEnabled=$rtp; AMServiceEnabled=$amsvc; AntispywareSignatureAge=$sigAge days; IsTamperProtected=$tamper" if (-not $rtp) { - Add-Finding -Id 'sec.defender.rtp_off' -Category 'security' -Severity 'critical' ` - -Title 'Defender real-time protection is OFF' ` - -Detail 'Real-time protection is disabled. The endpoint is unprotected against active threats. Re-enable immediately or confirm a managed 3rd-party AV is providing real-time protection.' ` - -Evidence $ev + if ($thirdPartyAv) { + # A known/managed 3rd-party AV is active; Windows disables Defender + # RTP by design when another AV registers. Expected, not a gap. + Add-Finding -Id 'sec.defender.rtp_off' -Category 'security' -Severity 'info' ` + -Title 'Defender real-time protection is OFF (3rd-party AV active)' ` + -Detail 'Defender real-time protection is off because a managed/known 3rd-party AV is active. Windows disables Defender real-time protection when another AV registers, so this is expected. Confirm the 3rd-party AV is providing real-time protection.' ` + -Evidence ($ev + '; ' + $thirdPartyAvEvidence) + } else { + Add-Finding -Id 'sec.defender.rtp_off' -Category 'security' -Severity 'critical' ` + -Title 'Defender real-time protection is OFF' ` + -Detail 'Real-time protection is disabled. The endpoint is unprotected against active threats. Re-enable immediately or confirm a managed 3rd-party AV is providing real-time protection.' ` + -Evidence $ev + } } if (-not $amsvc) { - Add-Finding -Id 'sec.defender.amservice_off' -Category 'security' -Severity 'critical' ` - -Title 'Defender antimalware service is not running' ` - -Detail 'The Defender antimalware service is not active. If no 3rd-party AV is present, this endpoint has no antivirus protection.' ` - -Evidence $ev + if ($thirdPartyAv) { + # Defender stands down its AM service when a 3rd-party AV registers. Expected. + Add-Finding -Id 'sec.defender.amservice_off' -Category 'security' -Severity 'info' ` + -Title 'Defender antimalware service is not running (3rd-party AV active)' ` + -Detail 'The Defender antimalware service is not active because a managed/known 3rd-party AV is registered. Windows stands Defender down when another AV provides protection, so this is expected. Confirm the 3rd-party AV is running.' ` + -Evidence ($ev + '; ' + $thirdPartyAvEvidence) + } else { + Add-Finding -Id 'sec.defender.amservice_off' -Category 'security' -Severity 'critical' ` + -Title 'Defender antimalware service is not running' ` + -Detail 'The Defender antimalware service is not active. If no 3rd-party AV is present, this endpoint has no antivirus protection.' ` + -Evidence $ev + } } if ($sigAge -lt 0) { # Sentinel: AntispywareSignatureAge could not be read. Never let this