fix(onboarding-diag): harden 3rd-party AV detection against false positives

Require SecurityCenter2 productState RTP-enabled bit before treating a
registered AV as active (lapsed/disabled AV no longer suppresses the
critical Defender finding), and tighten the Datto fallback to AV/EDR
services only — excluding Datto RMM/Backup/Workplace/Continuity/File so
non-AV Datto products can't masquerade as antivirus. Fix misleading comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 18:45:38 -07:00
parent 5ee92ad5b1
commit 959b3a159d

View File

@@ -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