Skip to content

Commit 3f8a429

Browse files
authored
SecOps - 41055 - Real-time, behavioral, and heuristic protection are enabled on Microsoft Defender Antivirus (#1504)
LGTM
2 parents 0308dc3 + 0826e8e commit 3f8a429

2 files changed

Lines changed: 284 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Microsoft Defender Antivirus uses cloud protection, always-on scanning with file and process behavior monitoring and heuristics, and protection updates to detect and block threats. Microsoft defines always-on protection as real-time protection, behavior monitoring, and heuristics that identify malware from known suspicious and malicious activities, such as unusual process changes to files, autostart registry keys, startup locations, and other file-system or file-structure changes. Real-time protection scans files when users open, download, or use them, so disabling these controls reduces endpoint protection before suspicious or malicious activity can be identified locally. Tamper protection helps keep always-on protection and other security settings from being changed, but this check does not evaluate tamper protection. Microsoft Secure Score measures organizational security posture and includes recommendations for Microsoft Defender for Endpoint; this check reads Secure Score control profiles and the latest Secure Score snapshot for the spec-pinned MDATP IDs `scid_2012`, `scid_91`, `scid_92`, `scid_89`, `scid_90`, `scid_5093`, and `scid_6093`.
2+
3+
**Remediation action**
4+
5+
- [Configure behavioral, heuristic, and real-time protection](https://learn.microsoft.com/en-us/defender-endpoint/configure-protection-features-microsoft-defender-antivirus)
6+
- [Enable and configure always-on protection](https://learn.microsoft.com/en-us/defender-endpoint/configure-real-time-protection-microsoft-defender-antivirus)
7+
- [Use Microsoft Intune to manage Defender Antivirus settings](https://learn.microsoft.com/en-us/mem/intune/protect/antivirus-microsoft-defender-settings-windows)
8+
9+
<!--- Results --->
10+
%TestResult%
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
<#
2+
.SYNOPSIS
3+
Real-time, behavioral, and heuristic protection are enabled on Microsoft Defender Antivirus.
4+
5+
.DESCRIPTION
6+
Evaluates the pinned Microsoft Defender Antivirus Secure Score controls by joining control
7+
profiles to the latest Microsoft Secure Score snapshot and comparing each achieved score with
8+
its maximum score. Controls with missing profiles, controls without applicable devices, and
9+
ignored controls are excluded from the roll-up.
10+
11+
.NOTES
12+
Test ID: 41055
13+
Workshop Task ID: SECOPS-055
14+
Category: Endpoint threat protection
15+
Pillar: SecOps
16+
Required Module: Microsoft.Graph.Authentication
17+
Required Connection: Microsoft Graph
18+
#>
19+
20+
function Test-Assessment-41055 {
21+
[ZtTest(
22+
Category = 'Endpoint threat protection',
23+
CompatibleLicense = ('WINDEFATP', 'MDE_LITE'),
24+
ImplementationCost = 'Low',
25+
Pillar = 'SecOps',
26+
RiskLevel = 'High',
27+
Service = ('Graph'),
28+
SfiPillar = 'Monitor and detect cyberthreats',
29+
TenantType = ('Workforce'),
30+
TestId = 41055,
31+
Title = 'Real-time, behavioral, and heuristic protection are enabled on Microsoft Defender Antivirus',
32+
UserImpact = 'Low'
33+
)]
34+
[CmdletBinding()]
35+
param()
36+
37+
#region Data Collection
38+
Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
39+
40+
$activity = 'Checking Microsoft Defender Antivirus protection controls in Microsoft Secure Score'
41+
$defenderAntivirusControlIds = @(
42+
'scid_2012', 'scid_91', 'scid_92', 'scid_89', 'scid_90', 'scid_5093', 'scid_6093'
43+
)
44+
$controlProfileError = $null
45+
$secureScoreError = $null
46+
47+
# Q1: Read MDATP Secure Score control profiles, then intersect client-side with pinned IDs.
48+
Write-ZtProgress -Activity $activity -Status 'Getting MDATP Secure Score control profiles'
49+
50+
$mdatpControlProfiles = @()
51+
try {
52+
$controlProfileFilter = "service eq 'MDATP' and (id eq 'scid_2012' or id eq 'scid_91' or id eq 'scid_92' or id eq 'scid_89' or id eq 'scid_90' or id eq 'scid_5093' or id eq 'scid_6093')"
53+
$mdatpControlProfiles = @(Invoke-ZtGraphRequest -RelativeUri 'security/secureScoreControlProfiles' -Filter $controlProfileFilter -ApiVersion beta -ErrorAction Stop)
54+
}
55+
catch {
56+
$controlProfileError = $_
57+
Write-PSFMessage "Failed to retrieve MDATP Secure Score control profiles: $_" -Tag Test -Level Warning
58+
}
59+
60+
# Q2: Read the latest Secure Score snapshot; -DisablePaging returns the wrapper object.
61+
Write-ZtProgress -Activity $activity -Status 'Getting latest Secure Score snapshot'
62+
63+
$latestSecureScore = $null
64+
try {
65+
$secureScoresResponse = Invoke-ZtGraphRequest -RelativeUri 'security/secureScores' -Top 1 -ApiVersion beta -DisablePaging -ErrorAction Stop
66+
$secureScores = @($secureScoresResponse.value)
67+
if ($secureScores.Count -gt 0) {
68+
$latestSecureScore = $secureScores[0]
69+
}
70+
}
71+
catch {
72+
$secureScoreError = $_
73+
Write-PSFMessage "Failed to retrieve latest Secure Score snapshot: $_" -Tag Test -Level Warning
74+
}
75+
#endregion Data Collection
76+
77+
#region Assessment Logic
78+
$passed = $false
79+
$customStatus = $null
80+
81+
foreach ($queryError in @($controlProfileError, $secureScoreError) | Where-Object { $null -ne $_ }) {
82+
if ((Get-ZtHttpStatusCode -ErrorRecord $queryError) -in (401, 403)) {
83+
$params = @{
84+
TestId = '41055'
85+
Title = 'Real-time, behavioral, and heuristic protection are enabled on Microsoft Defender Antivirus'
86+
Status = $false
87+
Result = '⚠️ Microsoft Graph returned HTTP 401 or 403. Verify SecurityEvents.Read.All is granted, Secure Score data is flowing, and at least one MDE device is onboarded.'
88+
CustomStatus = 'Investigate'
89+
}
90+
Add-ZtTestResultDetail @params
91+
return
92+
}
93+
}
94+
95+
$controlProfileById = @{}
96+
foreach ($controlProfile in @($mdatpControlProfiles | Where-Object { $defenderAntivirusControlIds -contains $_.id })) {
97+
if ($null -ne $controlProfile.id -and -not $controlProfileById.ContainsKey($controlProfile.id)) {
98+
$controlProfileById[$controlProfile.id] = $controlProfile
99+
}
100+
}
101+
102+
if ($controlProfileError -or $secureScoreError -or $null -eq $latestSecureScore) {
103+
$params = @{
104+
TestId = '41055'
105+
Title = 'Real-time, behavioral, and heuristic protection are enabled on Microsoft Defender Antivirus'
106+
Status = $false
107+
Result = '⚠️ No Microsoft Defender Antivirus Secure Score control could be evaluated; verify SecurityEvents.Read.All is granted, Secure Score data is flowing, and at least one MDE device is onboarded.'
108+
CustomStatus = 'Investigate'
109+
}
110+
Add-ZtTestResultDetail @params
111+
return
112+
}
113+
114+
$controlScoreByName = @{}
115+
foreach ($controlScore in @($latestSecureScore.controlScores)) {
116+
if ($null -ne $controlScore.controlName -and -not $controlScoreByName.ContainsKey($controlScore.controlName)) {
117+
$controlScoreByName[$controlScore.controlName] = $controlScore
118+
}
119+
}
120+
121+
$evaluationResults = @()
122+
foreach ($controlId in $defenderAntivirusControlIds) {
123+
$controlProfile = if ($controlProfileById.ContainsKey($controlId)) { $controlProfileById[$controlId] } else { $null }
124+
$matchingScore = if ($controlScoreByName.ContainsKey($controlId)) { $controlScoreByName[$controlId] } else { $null }
125+
126+
$latestStateUpdate = @()
127+
if ($null -ne $controlProfile) {
128+
$latestStateUpdate = @($controlProfile.controlStateUpdates | Sort-Object { if ($_.updatedDateTime) { [datetime]$_.updatedDateTime } else { [datetime]::MinValue } } -Descending | Select-Object -First 1)
129+
}
130+
$controlState = if ($latestStateUpdate.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($latestStateUpdate[0].state)) { $latestStateUpdate[0].state } else { 'N/A' }
131+
$isIgnored = $latestStateUpdate.Count -gt 0 -and $latestStateUpdate[0].state -eq 'ignored'
132+
133+
$score = if ($null -ne $matchingScore -and $null -ne $matchingScore.score) { $matchingScore.score } else { $null }
134+
$maxScore = if ($null -ne $controlProfile -and $null -ne $controlProfile.maxScore) { $controlProfile.maxScore } else { $null }
135+
$scoreValue = $null
136+
$scoreIsNumeric = $false
137+
if ($null -ne $score) {
138+
try {
139+
$scoreValue = [double]$score
140+
$scoreIsNumeric = $true
141+
}
142+
catch { }
143+
}
144+
145+
$maxScoreValue = $null
146+
$maxScoreIsNumeric = $false
147+
if ($null -ne $maxScore) {
148+
try {
149+
$maxScoreValue = [double]$maxScore
150+
$maxScoreIsNumeric = $true
151+
}
152+
catch { }
153+
}
154+
155+
$statusReason = $null
156+
$status = if ($null -eq $controlProfile) {
157+
$statusReason = 'profile not found'
158+
'N/A'
159+
}
160+
elseif ($null -eq $matchingScore) {
161+
$statusReason = 'no applicable devices'
162+
'N/A'
163+
}
164+
elseif ($isIgnored) {
165+
'Skipped'
166+
}
167+
elseif (-not $scoreIsNumeric -or -not $maxScoreIsNumeric) {
168+
'Investigate'
169+
}
170+
elseif ($scoreValue -ge $maxScoreValue) {
171+
'Pass'
172+
}
173+
else {
174+
'Fail'
175+
}
176+
177+
$controlTitle = if ($null -ne $controlProfile -and -not [string]::IsNullOrWhiteSpace($controlProfile.title)) {
178+
$controlProfile.title
179+
}
180+
else {
181+
$controlId
182+
}
183+
184+
$evaluationResults += [PSCustomObject]@{
185+
ControlId = $controlId
186+
ControlTitle = $controlTitle
187+
ActionUrl = if ($null -ne $controlProfile) { $controlProfile.actionUrl } else { $null }
188+
Score = if ($null -ne $score) { $score } else { 'N/A' }
189+
MaxScore = if ($null -ne $maxScore) { $maxScore } else { 'N/A' }
190+
ImplementationStatus = if ($null -ne $matchingScore -and -not [string]::IsNullOrWhiteSpace($matchingScore.implementationStatus)) { $matchingScore.implementationStatus } else { 'N/A' }
191+
State = $controlState
192+
LastModifiedDateTime = if ($null -ne $controlProfile) { $controlProfile.lastModifiedDateTime } else { $null }
193+
Status = $status
194+
StatusReason = $statusReason
195+
}
196+
}
197+
198+
$failedItems = @($evaluationResults | Where-Object Status -eq 'Fail')
199+
$investigateItems = @($evaluationResults | Where-Object Status -eq 'Investigate')
200+
$passedItems = @($evaluationResults | Where-Object Status -eq 'Pass')
201+
202+
if ($failedItems.Count -gt 0) {
203+
$testResultMarkdown = "❌ One or more of real-time, behavioral, or heuristic scanning is disabled.`n`n%TestResult%"
204+
}
205+
elseif ($investigateItems.Count -gt 0) {
206+
$customStatus = 'Investigate'
207+
$testResultMarkdown = "⚠️ One or more Microsoft Defender Antivirus Secure Score controls could not be evaluated.`n`n%TestResult%"
208+
}
209+
elseif ($passedItems.Count -gt 0) {
210+
$passed = $true
211+
$testResultMarkdown = "✅ Real-time, behavioral, and heuristic protection on Microsoft Defender Antivirus are enabled.`n`n%TestResult%"
212+
}
213+
else {
214+
$customStatus = 'Investigate'
215+
$testResultMarkdown = "⚠️ No Microsoft Defender Antivirus Secure Score control could be evaluated; verify SecurityEvents.Read.All is granted, Secure Score data is flowing, and at least one MDE device is onboarded.`n`n%TestResult%"
216+
}
217+
#endregion Assessment Logic
218+
219+
#region Report Generation
220+
$totalCount = $evaluationResults.Count
221+
$countLine = "Total pinned Microsoft Defender Antivirus controls: $totalCount"
222+
$secureScoreLink = '[Microsoft Secure Score](https://security.microsoft.com/securescore)'
223+
224+
$tableRows = ''
225+
foreach ($result in $evaluationResults) {
226+
$statusDisplay = switch ($result.Status) {
227+
'Pass' { '✅ Pass' }
228+
'Fail' { '❌ Fail' }
229+
'Investigate' { '⚠️ Investigate' }
230+
'N/A' { "N/A ($($result.StatusReason))" }
231+
default { 'Skipped' }
232+
}
233+
$lastModified = if ($result.LastModifiedDateTime) { Get-FormattedDate -DateString $result.LastModifiedDateTime } else { 'N/A' }
234+
$safeControlTitle = Get-SafeMarkdown -Text $result.ControlTitle
235+
$controlDisplay = if (-not [string]::IsNullOrWhiteSpace($result.ActionUrl)) {
236+
"[$safeControlTitle]($($result.ActionUrl)) ($($result.ControlId))"
237+
}
238+
elseif ($result.ControlTitle -ne $result.ControlId) {
239+
"$safeControlTitle ($($result.ControlId))"
240+
}
241+
else {
242+
$safeControlTitle
243+
}
244+
$tableRows += "| $controlDisplay | $($result.Score) | $($result.MaxScore) | $($result.ImplementationStatus) | $($result.State) | $lastModified | $statusDisplay |`n"
245+
}
246+
247+
$controlTable = @"
248+
| Control title (id) | Score | Max score | Implementation status | State | Last modified | Status |
249+
| :----------------- | ----: | --------: | :-------------------- | :---- | :------------ | :----- |
250+
$tableRows
251+
"@
252+
$formatTemplate = @'
253+
{0}
254+
255+
{1}
256+
257+
{2}
258+
'@
259+
$mdInfo = $formatTemplate -f $countLine, $secureScoreLink, $controlTable
260+
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
261+
#endregion Report Generation
262+
263+
$params = @{
264+
TestId = '41055'
265+
Title = 'Real-time, behavioral, and heuristic protection are enabled on Microsoft Defender Antivirus'
266+
Status = $passed
267+
Result = $testResultMarkdown
268+
}
269+
if ($customStatus) {
270+
$params.CustomStatus = $customStatus
271+
}
272+
273+
Add-ZtTestResultDetail @params
274+
}

0 commit comments

Comments
 (0)