tests/Test-Assessment.24550.ps1


<#
.SYNOPSIS
 
#>




function Test-Assessment-24550 {
    [ZtTest(
        Category = 'Devices',
        ImplementationCost = 'Low',
        CompatibleLicense = ('INTUNE_A'),
        Pillar = 'Devices',
        RiskLevel = 'High',
        SfiPillar = 'Protect engineering systems',
        TenantType = ('Workforce'),
        TestId = 24550,
        Title = 'Data on Windows is protected by BitLocker encryption',
        UserImpact = 'Low'
    )]
    [CmdletBinding()]
    param(
        $Database
    )

    #region Helper Functions
    function Get-AllSettingInstances {
        # Recursively collects every setting instance in a policy's settings tree,
        # including those nested inside groupSettingCollectionValue.children and
        # groupSettingValue.children paths used by Settings Catalog policies.
        param([array]$SettingInstances)
        $result = [System.Collections.Generic.List[object]]::new()
        foreach ($si in $SettingInstances) {
            if ($null -eq $si) { continue }
            $result.Add($si)
            if ($si.groupSettingCollectionValue) {
                foreach ($gsv in $si.groupSettingCollectionValue) {
                    if ($gsv.children) {
                        $result.AddRange([object[]](Get-AllSettingInstances -SettingInstances @($gsv.children)))
                    }
                }
            }
            if ($si.groupSettingValue -and $si.groupSettingValue.children) {
                $result.AddRange([object[]](Get-AllSettingInstances -SettingInstances @($si.groupSettingValue.children)))
            }
            if ($si.choiceSettingValue -and $si.choiceSettingValue.children) {
                $result.AddRange([object[]](Get-AllSettingInstances -SettingInstances @($si.choiceSettingValue.children)))
            }
            if ($si.choiceSettingCollectionValue) {
                foreach ($csv in $si.choiceSettingCollectionValue) {
                    if ($csv.children) {
                        $result.AddRange([object[]](Get-AllSettingInstances -SettingInstances @($csv.children)))
                    }
                }
            }
        }
        return $result
    }

    function Test-PolicyAssignment {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory = $false)]
            [array]$Policies
        )

        # Return false if $Policies is null or empty
        if (-not $Policies) {
            return $false
        }

        # Check if at least one policy has assignments
        $assignedPolicies = $Policies | Where-Object {
            $_.PSObject.Properties.Match("assignments") -and $_.assignments -and $_.assignments.Count -gt 0
        }

        return $assignedPolicies.Count -gt 0
    }
    #endregion Helper Functions

    #region Data Collection
    Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose

    $activity = "Checking Windows BitLocker policy is configured and assigned"
    Write-ZtProgress -Activity $activity -Status "Getting modern BitLocker policies"

    # Query 1: Retrieve modern Windows 10 Settings Catalog / Endpoint Security configuration policies from DB
    # templateReference is a DuckDB STRUCT — wrap in to_json() so PowerShell can access its properties.
    # Personal Data Encryption policies are excluded in the WHERE clause.
    $sql = @"
    SELECT id, name, platforms,
           to_json(templateReference) as templateReference,
           to_json(settings) as settings,
           to_json(assignments) as assignments
    FROM ConfigurationPolicy
    WHERE platforms LIKE '%windows10%'
      AND (templateReference.templateDisplayName NOT LIKE '%Personal Data Encryption%'
           OR templateReference.templateDisplayName IS NULL)
"@

    $modernPoliciesRaw = Invoke-DatabaseQuery -Database $Database -Sql $sql -AsCustomObject

    # Parse JSON fields
    foreach ($policy in $modernPoliciesRaw) {
        if ($policy.templateReference -is [string]) {
            $policy.templateReference = $policy.templateReference | ConvertFrom-Json
        }
        if ($policy.settings -is [string]) {
            $policy.settings = $policy.settings | ConvertFrom-Json
        }
        if ($policy.assignments -is [string]) {
            $policy.assignments = $policy.assignments | ConvertFrom-Json
        }
    }

    # Filter to BitLocker-relevant modern policies:
    # Either in the Endpoint Security Disk Encryption template family (excluding Personal Data Encryption),
    # or containing at least one device_vendor_msft_bitlocker setting.
    # Must also have Require Device Encryption enabled (value = _1).
    $windowsBitLockerPolicies = @()
    foreach ($policy in $modernPoliciesRaw) {
        $isEndpointSecurityDiskEncryption = $policy.templateReference.templateFamily -eq 'endpointSecurityDiskEncryption'

        # Recursively flatten all setting instances to catch settings nested inside
        # groupSettingCollectionValue.children (common in Settings Catalog BitLocker policies)
        $allInstances    = Get-AllSettingInstances -SettingInstances @($policy.settings.settingInstance)
        $allSettingIds   = @($allInstances | ForEach-Object { $_.settingDefinitionId } | Where-Object { $_ })

        $hasBitLockerSettings = ($allSettingIds | Where-Object { $_ -like 'device_vendor_msft_bitlocker*' }).Count -gt 0

        # Only proceed if this is a BitLocker-relevant policy
        if ($isEndpointSecurityDiskEncryption -or $hasBitLockerSettings) {
            # Check that Require Device Encryption is explicitly enabled anywhere in the setting tree
            $allChoiceValues = @($allInstances | ForEach-Object { $_.choiceSettingValue.value } | Where-Object { $_ })

            if ($allChoiceValues -contains 'device_vendor_msft_bitlocker_requiredeviceencryption_1') {
                $windowsBitLockerPolicies += $policy
            }
        }
    }

    Write-ZtProgress -Activity $activity -Status "Getting legacy BitLocker profiles"

    # Query 2: Retrieve legacy Windows device configuration profiles that enforce BitLocker / device encryption
    try {
        $deviceConfigsRaw = Invoke-ZtGraphRequest -RelativeUri "deviceManagement/deviceConfigurations?`$expand=assignments" `
            -ApiVersion beta -ErrorAction Stop
    }
    catch {
        Write-PSFMessage "Failed to retrieve legacy device configuration profiles: $_" -Tag Test -Level Warning
        $deviceConfigsRaw = @()
    }

    $legacyWindowsBitLockerProfiles = @()
    $legacyWindowsTypes = @(
        '#microsoft.graph.windows10EndpointProtectionConfiguration',
        '#microsoft.graph.windows10GeneralConfiguration'
    )
    foreach ($deviceConfig in $deviceConfigsRaw) {
        if ($deviceConfig.'@odata.type' -notin $legacyWindowsTypes) { continue }

        $enforcesBitLocker = (
            $deviceConfig.storageRequireDeviceEncryption -eq $true -or
            $deviceConfig.bitLockerEncryptDevice -eq $true -or
            $null -ne $deviceConfig.bitLockerSystemDrivePolicy -or
            $null -ne $deviceConfig.bitLockerFixedDrivePolicy -or
            $null -ne $deviceConfig.bitLockerRemovableDrivePolicy
        )

        if ($enforcesBitLocker) {
            $legacyWindowsBitLockerProfiles += $deviceConfig
        }
    }

    #endregion Data Collection

    #region Assessment Logic
    $passed = $false
    $testResultMarkdown = ""

    # Pass if at least one modern policy OR at least one legacy profile is assigned
    $modernAssigned  = Test-PolicyAssignment -Policies $windowsBitLockerPolicies
    $legacyAssigned  = Test-PolicyAssignment -Policies $legacyWindowsBitLockerProfiles
    $passed = $modernAssigned -or $legacyAssigned

    if ($passed) {
        $testResultMarkdown = "At least one Windows BitLocker policy is configured and assigned.`n`n%TestResult%"
    }
    else {
        $testResultMarkdown = "No Windows BitLocker policy is configured or assigned.`n`n%TestResult%"
    }
    #endregion Assessment Logic

    #region Report Generation
    $portalLink = 'https://intune.microsoft.com/#view/Microsoft_Intune_DeviceSettings/DevicesMenu/~/configuration'
    $mdInfo = ""

    # Modern BitLocker policies section
    if ($windowsBitLockerPolicies.Count -gt 0) {
        $modernRows = ""
        foreach ($policy in $windowsBitLockerPolicies) {
            $policyName = Get-SafeMarkdown -Text $policy.name
            $templateFamily = $policy.templateReference.templateFamily
            $requireDeviceEncryption = '✅ Enabled'
            $assignmentCount = $policy.assignments.Count
            if ($policy.assignments -and $policy.assignments.Count -gt 0) {
                $assignmentTarget = Get-PolicyAssignmentTarget -Assignments $policy.assignments
            }
            else {
                $assignmentTarget = 'None'
            }
            $modernRows += "| [$policyName]($portalLink) | $templateFamily | $requireDeviceEncryption | $assignmentCount | $assignmentTarget |`n"
        }

        $mdInfo += @"
 
## Modern Windows BitLocker Policies
 
| Policy Name | Template Family | Require Device Encryption | Assignments | Assignment Target |
| :---------- | :-------------- | :------------------------ | :---------- | :---------------- |
$modernRows
 
"@

    }

    # Legacy BitLocker profiles section
    if ($legacyWindowsBitLockerProfiles.Count -gt 0) {
        $legacyRows = ""
        foreach ($legacyProfile in $legacyWindowsBitLockerProfiles) {
            $profileName = Get-SafeMarkdown -Text $legacyProfile.displayName
            $odataType = $legacyProfile.'@odata.type' -replace '#microsoft.graph.', ''

            $encryptionFields = @()
            if ($legacyProfile.storageRequireDeviceEncryption -eq $true) { $encryptionFields += 'Device Encryption' }
            if ($legacyProfile.bitLockerEncryptDevice -eq $true) { $encryptionFields += 'BitLocker Encrypt Device' }
            if ($null -ne $legacyProfile.bitLockerSystemDrivePolicy) { $encryptionFields += 'System Drive Policy' }
            if ($null -ne $legacyProfile.bitLockerFixedDrivePolicy) { $encryptionFields += 'Fixed Drive Policy' }
            if ($null -ne $legacyProfile.bitLockerRemovableDrivePolicy) { $encryptionFields += 'Removable Drive Policy' }
            $encryptionSettings = $encryptionFields -join ', '

            $assignmentCount = $legacyProfile.assignments.Count
            if ($legacyProfile.assignments -and $legacyProfile.assignments.Count -gt 0) {
                $assignmentTarget = Get-PolicyAssignmentTarget -Assignments $legacyProfile.assignments
            }
            else {
                $assignmentTarget = 'None'
            }
            $legacyRows += "| [$profileName]($portalLink) | $odataType | $encryptionSettings | $assignmentCount | $assignmentTarget |`n"
        }

        $mdInfo += @"
 
## Legacy Windows BitLocker Profiles
 
| Profile Name | Profile Type | Encryption Settings | Assignments | Assignment Target |
| :----------- | :----------- | :------------------ | :---------- | :---------------- |
$legacyRows
 
"@

    }

    # Replace the placeholder with the detailed information
    $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo
    #endregion Report Generation

    $params = @{
        TestId = '24550'
        Title  = 'Data on Windows is protected by BitLocker encryption'
        Status = $passed
        Result = $testResultMarkdown
    }

    Add-ZtTestResultDetail @params
}