tests/Test-Assessment.41049.ps1
|
<#
.SYNOPSIS The Microsoft Defender Antivirus user interface is hidden from end users. .DESCRIPTION Checks whether the DisableVirusUI setting is configured and assigned in Intune policies (Settings Catalog or legacy endpoint protection) to hide the Virus and threat protection UI from standard users. .NOTES Test ID: 41049 Workshop Task ID: SECOPS-049 Category: Endpoint threat protection Pillar: SecOps Required Module: Microsoft.Graph.Authentication Required Connection: Microsoft Graph Required Permission: DeviceManagementConfiguration.Read.All #> function Test-Assessment-41049 { [ZtTest( Category = 'Endpoint threat protection', CompatibleLicense = ('INTUNE_A&WINDEFATP'), ImplementationCost = 'Low', Pillar = 'SecOps', RiskLevel = 'Medium', Service = ('Graph'), SfiPillar = 'Monitor and detect cyberthreats', TenantType = ('Workforce'), TestId = 41049, Title = 'The Microsoft Defender Antivirus user interface is hidden from end users', UserImpact = 'Medium' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking Defender UI suppression configuration' $evaluationResults = @() $settingsCatalogError = $null $legacyEppError = $null $hasPolicyReadError = $false # Q1: List Settings Catalog configuration policies from Intune and check for Defender UI suppression settings. try { Write-ZtProgress -Activity $activity -Status 'Getting Settings Catalog policies' $settingsCatalogPolicies = @(Invoke-ZtGraphRequest -RelativeUri 'deviceManagement/configurationPolicies' -ApiVersion beta -ErrorAction Stop) $settingsCatalogPolicies = @($settingsCatalogPolicies | Where-Object { $_.technologies -match '(?i)\bmdm\b' }) Write-PSFMessage "Found $($settingsCatalogPolicies.Count) Settings Catalog policies" -Level Verbose } catch { $settingsCatalogError = $_ Write-PSFMessage "Failed to query Settings Catalog policies: $_" -Tag Test -Level Warning } # Process each Settings Catalog policy foreach ($policy in $settingsCatalogPolicies) { try { Write-ZtProgress -Activity $activity -Status "Reading Settings Catalog policy $($policy.name)" $settings = @(Invoke-ZtGraphRequest -RelativeUri "deviceManagement/configurationPolicies/$($policy.id)/settings" -ApiVersion beta -ErrorAction Stop) $assignments = @(Invoke-ZtGraphRequest -RelativeUri "deviceManagement/configurationPolicies/$($policy.id)/assignments" -ApiVersion beta -ErrorAction Stop) # Flatten nested children: top-level items are {id, settingInstance} wrappers; # children inside choiceSettingValue are bare settingInstance objects; # groupSettingCollectionValue items are GroupSettingValue wrappers whose .children hold the nested settingInstances. $flattenedSettings = [System.Collections.Generic.List[object]]::new() $visitSettingInstances = { param($instances) foreach ($instance in @($instances)) { if ($null -eq $instance) { continue } [void]$flattenedSettings.Add($instance) $childInstances = @($instance.settingInstance.choiceSettingValue.children | Where-Object { $_ }) foreach ($csv in @($instance.settingInstance.choiceSettingCollectionValue | Where-Object { $_ })) { $childInstances += @($csv.children | Where-Object { $_ }) } foreach ($gsv in @($instance.settingInstance.groupSettingCollectionValue | Where-Object { $_ })) { $childInstances += @($gsv.children | Where-Object { $_ }) } $childInstances += @($instance.settingInstance.groupSettingValue.children | Where-Object { $_ }) $wrapped = @($childInstances | ForEach-Object { [PSCustomObject]@{ settingInstance = $_ } }) if ($wrapped.Count -gt 0) { & $visitSettingInstances $wrapped } } } & $visitSettingInstances $settings $settings = @($flattenedSettings) # Helper to find setting by ID pattern $findSetting = { param([string]$Pattern) @($settings | Where-Object { $_.settingInstance.settingDefinitionId -match $Pattern }) } $assignmentCount = $assignments.Count # Extract all UI suppression controls for context $uiSuppressionControls = @( @{ Name = 'Virus/AV UI'; Pattern = '(?i)_disablevirusui$'; IsPrimary = $true }, @{ Name = 'Account UI'; Pattern = '(?i)_disableaccountprotectionui$'; IsPrimary = $false }, @{ Name = 'App/browser UI'; Pattern = '(?i)_disableappbrowserui$'; IsPrimary = $false }, @{ Name = 'Device security UI'; Pattern = '(?i)_disabledevicesecurityui$'; IsPrimary = $false }, @{ Name = 'Family UI'; Pattern = '(?i)_disablefamilyui$'; IsPrimary = $false }, @{ Name = 'Health UI'; Pattern = '(?i)_disablehealthui$'; IsPrimary = $false }, @{ Name = 'Network UI'; Pattern = '(?i)_disablenetworkui$'; IsPrimary = $false }, @{ Name = 'Clear TPM button'; Pattern = '(?i)_disablecleartpmbutton$'; IsPrimary = $false }, @{ Name = 'TPM firmware warning'; Pattern = '(?i)_disabletpmfirmwareupdatewarning$'; IsPrimary = $false }, @{ Name = 'Ransomware recovery UI'; Pattern = '(?i)_hideransomwaredatarecovery$'; IsPrimary = $false }, @{ Name = 'Secure Boot UI'; Pattern = '(?i)_hidesecureboot$'; IsPrimary = $false }, @{ Name = 'TPM troubleshooting UI'; Pattern = '(?i)_hidetpmtroubleshooting$'; IsPrimary = $false }, @{ Name = 'Notification-area icon'; Pattern = '(?i)_hidewindowssecuritynotificationareacontrol$'; IsPrimary = $false }, @{ Name = 'Noncritical notifications'; Pattern = '(?i)_disableenhancednotifications$'; IsPrimary = $false }, @{ Name = 'All notifications'; Pattern = '(?i)_disablenotifications$'; IsPrimary = $false } ) foreach ($control in $uiSuppressionControls) { $matchingSetting = & $findSetting $control.Pattern if ($matchingSetting.Count -eq 0) { continue } foreach ($setting in $matchingSetting) { $settingId = $setting.settingInstance.settingDefinitionId # Extract the normalized value: _1 = enabled/hidden, _0 = disabled/visible $choiceValue = [string]$setting.settingInstance.choiceSettingValue.value $normalizedState = 'N/A' if ($choiceValue -match '(?i)_1$') { $normalizedState = 'Hidden' } elseif ($choiceValue -match '(?i)_0$') { $normalizedState = 'Visible' } # Only DisableVirusUI (IsPrimary) determines overall outcome; other controls are contextual. $controlStatus = 'N/A' if ($normalizedState -eq 'N/A') { $controlStatus = 'Investigate' } elseif ($normalizedState -eq 'Hidden' -and $assignmentCount -gt 0) { $controlStatus = 'Pass' } else { $controlStatus = 'Fail' } $evaluationResults += [PSCustomObject]@{ PolicyName = $policy.name Source = 'Settings Catalog' AssignmentCount = $assignmentCount CanonicalControl = $control.Name RawSettingProperty = $settingId NormalizedState = $normalizedState Status = $controlStatus IsPrimary = $control.IsPrimary } } } } catch { Write-PSFMessage "Failed to read Settings Catalog policy '$($policy.name)': $_" -Tag Test -Level Warning $hasPolicyReadError = $true } } # Q2: Legacy windows10EndpointProtectionConfiguration (fallback if Q1 found no primary DisableVirusUI result) $q1HasPrimary = ($evaluationResults | Where-Object { $_.IsPrimary -eq $true }).Count -gt 0 if (-not $q1HasPrimary) { Write-ZtProgress -Activity $activity -Status 'Getting legacy endpoint protection configuration policies' try { $legacyEppUri = "deviceManagement/deviceConfigurations?`$filter=isof('microsoft.graph.windows10EndpointProtectionConfiguration')" $legacyEppPolicies = @(Invoke-ZtGraphRequest -RelativeUri $legacyEppUri -ApiVersion beta -ErrorAction Stop) Write-PSFMessage "Found $($legacyEppPolicies.Count) legacy endpoint protection policies" -Level Verbose } catch { $legacyEppError = $_ Write-PSFMessage "Failed to query legacy EPP policies: $_" -Tag Test -Level Warning } # Process each legacy EPP policy foreach ($policy in $legacyEppPolicies) { try { Write-ZtProgress -Activity $activity -Status "Reading EPP policy $($policy.displayName)" $policy = Invoke-ZtGraphRequest -RelativeUri 'deviceManagement/deviceConfigurations' -UniqueId $policy.id -ApiVersion beta -ErrorAction Stop $assignments = @(Invoke-ZtGraphRequest -RelativeUri "deviceManagement/deviceConfigurations/$($policy.id)/assignments" -ApiVersion beta -ErrorAction Stop) $assignmentCount = $assignments.Count # Map legacy boolean properties to UI suppression controls $legacyControls = @( @{ Name = 'Virus/AV UI'; Property = 'defenderSecurityCenterDisableVirusUI'; IsPrimary = $true }, @{ Name = 'Account UI'; Property = 'defenderSecurityCenterDisableAccountUI'; IsPrimary = $false }, @{ Name = 'App/browser UI'; Property = 'defenderSecurityCenterDisableAppBrowserUI'; IsPrimary = $false }, @{ Name = 'Device security UI'; Property = 'defenderSecurityCenterDisableHardwareUI'; IsPrimary = $false }, @{ Name = 'Family UI'; Property = 'defenderSecurityCenterDisableFamilyUI'; IsPrimary = $false }, @{ Name = 'Health UI'; Property = 'defenderSecurityCenterDisableHealthUI'; IsPrimary = $false }, @{ Name = 'Network UI'; Property = 'defenderSecurityCenterDisableNetworkUI'; IsPrimary = $false }, @{ Name = 'Clear TPM button'; Property = 'defenderSecurityCenterDisableClearTpmUI'; IsPrimary = $false }, @{ Name = 'TPM firmware warning'; Property = 'defenderSecurityCenterDisableVulnerableTpmFirmwareUpdateUI'; IsPrimary = $false }, @{ Name = 'Ransomware recovery UI'; Property = 'defenderSecurityCenterDisableRansomwareUI'; IsPrimary = $false }, @{ Name = 'Secure Boot UI'; Property = 'defenderSecurityCenterDisableSecureBootUI'; IsPrimary = $false }, @{ Name = 'TPM troubleshooting UI'; Property = 'defenderSecurityCenterDisableTroubleshootingUI'; IsPrimary = $false }, @{ Name = 'Notification-area icon'; Property = 'defenderSecurityCenterDisableNotificationAreaUI'; IsPrimary = $false }, @{ Name = 'Noncritical notifications'; Property = 'defenderSecurityCenterNotificationsFromApp'; ExpectedValue = 'blockNoncriticalNotifications'; IsPrimary = $false }, @{ Name = 'All notifications'; Property = 'defenderSecurityCenterNotificationsFromApp'; ExpectedValue = 'blockAllNotifications'; IsPrimary = $false } ) foreach ($control in $legacyControls) { $propertyValue = $policy.($control.Property) if ($null -eq $propertyValue) { continue } $normalizedState = 'N/A' if ($propertyValue -eq $true) { $normalizedState = 'Hidden' } elseif ($propertyValue -eq $false) { $normalizedState = 'Visible' } elseif ($control.ExpectedValue) { # blockAllNotifications suppresses all notifications including noncritical (superset). if ($propertyValue -eq $control.ExpectedValue -or $propertyValue -eq 'blockAllNotifications') { $normalizedState = 'Hidden' } elseif ($propertyValue -in @('blockNoncriticalNotifications', 'blockAllNotifications')) { $normalizedState = 'Visible' } } # Only DisableVirusUI (IsPrimary) determines overall outcome; other controls are contextual. $controlStatus = 'N/A' if ($normalizedState -eq 'N/A') { $controlStatus = 'Investigate' } elseif ($normalizedState -eq 'Hidden' -and $assignmentCount -gt 0) { $controlStatus = 'Pass' } else { $controlStatus = 'Fail' } $evaluationResults += [PSCustomObject]@{ PolicyName = $policy.displayName Source = 'Legacy EPP' AssignmentCount = $assignmentCount CanonicalControl = $control.Name RawSettingProperty = $control.Property NormalizedState = $normalizedState Status = $controlStatus IsPrimary = $control.IsPrimary } } } catch { Write-PSFMessage "Failed to read EPP policy '$($policy.displayName)': $_" -Tag Test -Level Warning $hasPolicyReadError = $true } } } #endregion Data Collection #region Assessment Logic # Q1 error is only fatal when Q2 also failed to recover any data. $hasQueryError = ($evaluationResults.Count -eq 0) -and (($null -ne $settingsCatalogError) -or ($null -ne $legacyEppError)) if ($hasQueryError) { $params = @{ TestId = '41049' Title = 'The Microsoft Defender Antivirus user interface is hidden from end users' Status = $false CustomStatus = 'Investigate' Result = '⚠️ Unable to determine DisableVirusUI status due to an API or access error. Re-run the assessment after verifying Intune licensing, DeviceManagementConfiguration.Read.All consent, and Microsoft Graph access.' } Add-ZtTestResultDetail @params return } # Determine overall outcome based on DisableVirusUI primary control $primaryResults = @($evaluationResults | Where-Object { $_.IsPrimary -eq $true }) $passed = $false $customStatus = $null if ($primaryResults.Count -eq 0) { # No DisableVirusUI control found on either surface $customStatus = 'Investigate' $suffix = if ($evaluationResults.Count -gt 0) { "`n`n%TestResult%" } else { '' } $testResultMarkdown = "⚠️ The ``DisableVirusUI`` state could not be determined on either the Settings Catalog or legacy endpoint protection surface; verify configuration in the Intune portal.$suffix" } else { # Evaluate with precedence: Fail > Investigate > Pass $failResults = @($primaryResults | Where-Object { $_.Status -eq 'Fail' }) $investigateResults = @($primaryResults | Where-Object { $_.Status -eq 'Investigate' }) if ($failResults.Count -gt 0) { $testResultMarkdown = "❌ The Defender Antivirus UI is not hidden — ``DisableVirusUI`` is disabled, or enabled only in an unassigned policy.`n`n%TestResult%" } elseif ($investigateResults.Count -gt 0) { $customStatus = 'Investigate' $testResultMarkdown = "⚠️ Could not determine ``DisableVirusUI`` state: no matching control on either surface, empty result set, uninterpretable value, or Graph 401/403/404.`n`n%TestResult%" } else { $passResults = @($primaryResults | Where-Object { $_.Status -eq 'Pass' }) if ($passResults.Count -gt 0) { if ($null -ne $settingsCatalogError -or $hasPolicyReadError) { # Incomplete data — Pass cannot be confirmed when some policies could not be read. $customStatus = 'Investigate' $testResultMarkdown = "⚠️ A policy has the Defender Antivirus UI hidden, but not all Intune policies could be read. Verify in the Intune portal that ``DisableVirusUI`` is configured and assigned, then re-run the assessment.`n`n%TestResult%" } else { $passed = $true $testResultMarkdown = "✅ The Microsoft Defender Antivirus (Virus & threat protection) UI is hidden from end users — an assigned policy enables `DisableVirusUI`.`n`n%TestResult%" } } } } #endregion Assessment Logic #region Report Generation if ($evaluationResults.Count -gt 0) { # Sort results: DisableVirusUI first (primary), then others by policy and control name, limit to 10 $sortedResults = @($evaluationResults | Sort-Object -Property { $_.IsPrimary -eq $false }, PolicyName, CanonicalControl | Select-Object -First 10) $portalUrl = 'https://intune.microsoft.com/#view/Microsoft_Intune_DeviceSettings/DevicesMenu/~/configuration' # Map status to emoji format $statusEmoji = @{ 'Pass' = '✅ Pass' 'Fail' = '❌ Fail' 'Investigate' = '⚠️ Investigate' 'N/A' = 'N/A' } $tableRows = @($sortedResults | ForEach-Object { $policyName = (Get-SafeMarkdown -Text $_.PolicyName) -replace '\|', '\\|' $policyLink = "$portalUrl" $displayStatus = $statusEmoji[$_.Status] if (-not $displayStatus) { $displayStatus = $_.Status } "| [$policyName]($policyLink) | $($_.Source) | $($_.AssignmentCount) | $($_.CanonicalControl) | $($_.RawSettingProperty) | $($_.NormalizedState) | $displayStatus |" }) if ($evaluationResults.Count -gt 10) { $tableRows += "| ... | | | | | | $($evaluationResults.Count) total configurations |" } $mdInfo = @" ## [Intune configuration policies](https://intune.microsoft.com/#view/Microsoft_Intune_DeviceSettings/DevicesMenu/~/configuration) | Policy/Profile | Source | Assignment Count | Canonical control | Raw setting/property | Normalized state | Status | | :--- | :--- | ---: | :--- | :--- | :--- | :--- | $($tableRows -join "`n") "@ $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo } $params = @{ TestId = '41049' Title = 'The Microsoft Defender Antivirus user interface is hidden from end users' Status = $passed Result = $testResultMarkdown } if ($null -ne $customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params #endregion Report Generation } |