|
| 1 | +<# |
| 2 | +.SYNOPSIS |
| 3 | + EDR in block mode is enabled when Microsoft Defender Antivirus is not the primary antivirus. |
| 4 | +
|
| 5 | +.DESCRIPTION |
| 6 | + Validates the Microsoft Defender for Endpoint "Enable EDR in block mode" Secure Score |
| 7 | + recommendation via Microsoft Secure Score. |
| 8 | +
|
| 9 | + EDR in block mode adds Microsoft Defender for Endpoint blocking when Microsoft Defender |
| 10 | + Antivirus is running in passive mode alongside a non-Microsoft antivirus solution. |
| 11 | + Malicious artifacts that the third-party antivirus misses may not be remediated without it. |
| 12 | +
|
| 13 | + The check reads the Secure Score control profile for scid_2004 and the latest per-control |
| 14 | + score snapshot, then returns: |
| 15 | + Pass – EDR in block mode is enabled. |
| 16 | + Fail – EDR in block mode is disabled or the Secure Score control is ignored. |
| 17 | + Investigate – The control profile or Secure Score snapshot could not be located. |
| 18 | + Investigate – Microsoft Graph returned HTTP 401 or 403 (insufficient permissions). |
| 19 | +
|
| 20 | +.NOTES |
| 21 | + Test ID: 41059 |
| 22 | + Workshop Task: SECOPS-059 |
| 23 | + Pillar: SecOps |
| 24 | + Category: Endpoint threat protection |
| 25 | + Risk Level: High |
| 26 | + Supported Clouds: Global, USGov, USGovDoD |
| 27 | + Required Permission: SecurityEvents.Read.All (Application or Delegated) |
| 28 | +#> |
| 29 | + |
| 30 | +function Test-Assessment-41059 { |
| 31 | + [ZtTest( |
| 32 | + Category = 'Endpoint threat protection', |
| 33 | + CompatibleLicense = ('WINDEFATP'), |
| 34 | + ImplementationCost = 'Low', |
| 35 | + Pillar = 'SecOps', |
| 36 | + RiskLevel = 'High', |
| 37 | + Service = ('Graph'), |
| 38 | + SfiPillar = 'Monitor and detect cyberthreats', |
| 39 | + TenantType = ('Workforce'), |
| 40 | + TestId = 41059, |
| 41 | + Title = 'EDR in block mode is enabled when Microsoft Defender Antivirus is not the primary antivirus', |
| 42 | + UserImpact = 'Low' |
| 43 | + )] |
| 44 | + [CmdletBinding()] |
| 45 | + param() |
| 46 | + |
| 47 | + #region Data Collection |
| 48 | + |
| 49 | + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose |
| 50 | + $activity = 'Checking EDR in block mode Secure Score control' |
| 51 | + $controlId = 'scid_2004' |
| 52 | + Write-ZtProgress -Activity $activity -Status 'Retrieving EDR in block mode Secure Score control profile' |
| 53 | + |
| 54 | + # Q1 – Retrieve the pinned MDATP "Enable EDR in block mode" control profile. |
| 55 | + # An empty result set indicates the profile is absent from this tenant's Secure Score. |
| 56 | + $controlProfile = $null |
| 57 | + $errorMsgQ1 = $null |
| 58 | + $httpStatusQ1 = $null |
| 59 | + |
| 60 | + try { |
| 61 | + $profileResults = Invoke-ZtGraphRequest -RelativeUri 'security/secureScoreControlProfiles' -Filter "service eq 'MDATP' and id eq '$controlId'" -ApiVersion beta -ErrorAction Stop |
| 62 | + $controlProfile = $profileResults | Select-Object -First 1 |
| 63 | + } |
| 64 | + catch { |
| 65 | + $errorMsgQ1 = $_ |
| 66 | + $httpStatusQ1 = Get-ZtHttpStatusCode -ErrorRecord $_ |
| 67 | + Write-PSFMessage "Failed to retrieve EDR in block mode control profile: $errorMsgQ1" -Level Warning |
| 68 | + } |
| 69 | + |
| 70 | + # Q2 – Retrieve the most recent Secure Score snapshot (only if Q1 succeeded). |
| 71 | + $latestSecureScore = $null |
| 72 | + $errorMsgQ2 = $null |
| 73 | + |
| 74 | + if ($null -ne $controlProfile) { |
| 75 | + Write-ZtProgress -Activity $activity -Status 'Retrieving latest Microsoft Secure Score' |
| 76 | + try { |
| 77 | + $scoreResponse = Invoke-ZtGraphRequest -RelativeUri 'security/secureScores' -Top 1 -ApiVersion beta -DisablePaging -ErrorAction Stop |
| 78 | + $latestSecureScore = $scoreResponse.value | Select-Object -First 1 |
| 79 | + } |
| 80 | + catch { |
| 81 | + $errorMsgQ2 = $_ |
| 82 | + Write-PSFMessage "Failed to retrieve Secure Score snapshot: $errorMsgQ2" -Level Warning |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + #endregion Data Collection |
| 87 | + |
| 88 | + #region Assessment Logic |
| 89 | + |
| 90 | + $passed = $false |
| 91 | + $customStatus = $null |
| 92 | + |
| 93 | + # ── Q1 failed: 401/403, unexpected error, or missing profile → Investigate ── |
| 94 | + if ($null -eq $controlProfile) { |
| 95 | + if ($httpStatusQ1 -in @(401, 403)) { |
| 96 | + $investigateReason = 'The **SecurityEvents.Read.All** permission is required to read Secure Score control profiles. Verify the permission is consented and re-run the assessment.' |
| 97 | + } |
| 98 | + elseif ($null -ne $errorMsgQ1) { |
| 99 | + $investigateReason = 'Microsoft Graph returned an unexpected error retrieving the EDR in block mode Secure Score control profile. Re-run the assessment in 5–10 minutes and verify **SecurityEvents.Read.All** is consented.' |
| 100 | + } |
| 101 | + else { |
| 102 | + $investigateReason = 'The EDR in block mode Secure Score control or latest Secure Score snapshot could not be located.' |
| 103 | + } |
| 104 | + |
| 105 | + $testResultMarkdown = "⚠️ $investigateReason" |
| 106 | + $customStatus = 'Investigate' |
| 107 | + |
| 108 | + $params = @{ |
| 109 | + TestId = '41059' |
| 110 | + Title = 'EDR in block mode is enabled when Microsoft Defender Antivirus is not the primary antivirus' |
| 111 | + Status = $passed |
| 112 | + Result = $testResultMarkdown |
| 113 | + CustomStatus = $customStatus |
| 114 | + } |
| 115 | + Add-ZtTestResultDetail @params |
| 116 | + return |
| 117 | + } |
| 118 | + |
| 119 | + $profileTitle = $controlProfile.title |
| 120 | + |
| 121 | + # ── Investigate: Q2 failed or returned no snapshot ── |
| 122 | + if ($null -ne $errorMsgQ2 -or $null -eq $latestSecureScore) { |
| 123 | + if ($null -ne $errorMsgQ2) { |
| 124 | + $investigateReason = 'Microsoft Graph returned an unexpected error retrieving the latest Secure Score snapshot. Re-run the assessment in 5–10 minutes.' |
| 125 | + } |
| 126 | + else { |
| 127 | + $investigateReason = 'The EDR in block mode Secure Score control or latest Secure Score snapshot could not be located.' |
| 128 | + } |
| 129 | + $testResultMarkdown = "⚠️ $investigateReason" |
| 130 | + $customStatus = 'Investigate' |
| 131 | + |
| 132 | + $params = @{ |
| 133 | + TestId = '41059' |
| 134 | + Title = 'EDR in block mode is enabled when Microsoft Defender Antivirus is not the primary antivirus' |
| 135 | + Status = $passed |
| 136 | + Result = $testResultMarkdown |
| 137 | + CustomStatus = $customStatus |
| 138 | + } |
| 139 | + Add-ZtTestResultDetail @params |
| 140 | + return |
| 141 | + } |
| 142 | + |
| 143 | + # ── Locate the per-control entry inside controlScores[] ── |
| 144 | + $controlScoreEntry = $null |
| 145 | + if ($latestSecureScore.controlScores) { |
| 146 | + $controlScoreEntry = $latestSecureScore.controlScores | |
| 147 | + Where-Object { $_.controlName -eq $controlId } | |
| 148 | + Select-Object -First 1 |
| 149 | + } |
| 150 | + |
| 151 | + # ── Investigate: profile exists but snapshot has no scored entry for this control, |
| 152 | + # or score/maxScore is null (would produce a false Pass via PowerShell $null coercion) ── |
| 153 | + $score = $controlScoreEntry.score |
| 154 | + $maxScore = $controlProfile.maxScore |
| 155 | + |
| 156 | + if ($null -eq $controlScoreEntry -or $null -eq $score -or $null -eq $maxScore) { |
| 157 | + $testResultMarkdown = '⚠️ The EDR in block mode Secure Score control or latest Secure Score snapshot could not be located.' |
| 158 | + $customStatus = 'Investigate' |
| 159 | + |
| 160 | + $params = @{ |
| 161 | + TestId = '41059' |
| 162 | + Title = 'EDR in block mode is enabled when Microsoft Defender Antivirus is not the primary antivirus' |
| 163 | + Status = $passed |
| 164 | + Result = $testResultMarkdown |
| 165 | + CustomStatus = $customStatus |
| 166 | + } |
| 167 | + Add-ZtTestResultDetail @params |
| 168 | + return |
| 169 | + } |
| 170 | + |
| 171 | + # ── Determine ignored state from the most recent controlStateUpdates entry ── |
| 172 | + $latestStateUpdate = @($controlProfile.controlStateUpdates | Sort-Object { if ($_.updatedDateTime) { [datetime]$_.updatedDateTime } else { [datetime]::MinValue } } -Descending) | Select-Object -First 1 |
| 173 | + $isIgnored = $latestStateUpdate -and $latestStateUpdate.state -eq 'Ignored' |
| 174 | + |
| 175 | + if (($score -ge $maxScore) -and (-not $isIgnored)) { |
| 176 | + $passed = $true |
| 177 | + $testResultMarkdown = "✅ EDR in block mode is enabled.`n`n%TestResult%" |
| 178 | + } |
| 179 | + else { |
| 180 | + $passed = $false |
| 181 | + $testResultMarkdown = "❌ EDR in block mode is disabled or the Secure Score control is ignored.`n`n%TestResult%" |
| 182 | + } |
| 183 | + |
| 184 | + #endregion Assessment Logic |
| 185 | + |
| 186 | + #region Report Generation |
| 187 | + |
| 188 | + $secureScoreUrl = 'https://security.microsoft.com/securescore' |
| 189 | + $ignoredDisplay = if ($isIgnored) { '⚠️ Yes' } else { '✅ No' } |
| 190 | + $rowStatus = if ($passed) { '✅ Pass' } else { '❌ Fail' } |
| 191 | + $lastModified = if ($controlProfile.lastModifiedDateTime) { Get-FormattedDate -DateString $controlProfile.lastModifiedDateTime } else { '—' } |
| 192 | + |
| 193 | + $tableRows = "| $(Get-SafeMarkdown $profileTitle) | $controlId | $score | $maxScore | $ignoredDisplay | $lastModified | $rowStatus |`n" |
| 194 | + |
| 195 | + $mdTable = @" |
| 196 | +
|
| 197 | +
|
| 198 | +### [Microsoft Secure Score]($secureScoreUrl) |
| 199 | +
|
| 200 | +| Control title | Control id | Score | Max score | Ignored | Last modified | Status | |
| 201 | +| :------------ | :--------- | :---- | :-------- | :------ | :------------ | :----- | |
| 202 | +$tableRows |
| 203 | +"@ |
| 204 | + |
| 205 | + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdTable |
| 206 | + |
| 207 | + #endregion Report Generation |
| 208 | + |
| 209 | + $params = @{ |
| 210 | + TestId = '41059' |
| 211 | + Title = 'EDR in block mode is enabled when Microsoft Defender Antivirus is not the primary antivirus' |
| 212 | + Status = $passed |
| 213 | + Result = $testResultMarkdown |
| 214 | + } |
| 215 | + if ($customStatus) { |
| 216 | + $params.CustomStatus = $customStatus |
| 217 | + } |
| 218 | + |
| 219 | + Add-ZtTestResultDetail @params |
| 220 | +} |
0 commit comments