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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user