Public/Get-IntunePolicyAssignmentReview.ps1

function Get-IntunePolicyAssignmentReview {
    <#
    .SYNOPSIS
        Reviews Intune configuration profiles and compliance policies for assignment gaps.
    .DESCRIPTION
        Enumerates device configuration profiles and compliance policies, checks for
        unassigned policies, policies assigned to empty groups, and conflicting policy
        settings. Helps identify configuration drift and policy sprawl.
    .PARAMETER IncludeCompliance
        Include compliance policies in addition to configuration profiles. Defaults to true.
    .EXAMPLE
        Get-IntunePolicyAssignmentReview
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [bool]$IncludeCompliance = $true
    )

    begin {
        Test-GraphConnection
        $results = [System.Collections.Generic.List[PSObject]]::new()
    }

    process {
        # --- Configuration Profiles ---
        Write-Verbose "Retrieving device configuration profiles..."
        $configProfiles = Get-MgDeviceManagementDeviceConfiguration -All -Property @(
            'id', 'displayName', '@odata.type', 'lastModifiedDateTime',
            'createdDateTime', 'version'
        )

        foreach ($profile in $configProfiles) {
            $findings = @()
            $profileType = ($profile.AdditionalProperties.'@odata.type' ?? $profile.'@odata.type') -replace '#microsoft.graph.', ''

            # Get assignments
            try {
                $assignments = Get-MgDeviceManagementDeviceConfigurationAssignment -DeviceConfigurationId $profile.Id -ErrorAction Stop
                $assignmentCount = ($assignments | Measure-Object).Count
            }
            catch {
                $assignments = @()
                $assignmentCount = 0
            }

            if ($assignmentCount -eq 0) {
                $findings += 'UNASSIGNED'
            }

            # Check assignment targets
            $targetTypes = @()
            foreach ($assignment in $assignments) {
                $targetType = $assignment.Target.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', ''
                $targetTypes += $targetType

                if ($targetType -eq 'allDevicesAssignmentTarget') {
                    $findings += 'ASSIGNED TO ALL DEVICES'
                }
                if ($targetType -eq 'allLicensedUsersAssignmentTarget') {
                    $findings += 'ASSIGNED TO ALL USERS'
                }
            }

            # Check for stale profiles
            if ($profile.LastModifiedDateTime) {
                $daysSinceModified = [math]::Round(((Get-Date) - $profile.LastModifiedDateTime).TotalDays)
                if ($daysSinceModified -gt 365) {
                    $findings += "NOT MODIFIED IN $daysSinceModified DAYS"
                }
            }

            # Get device status summary
            try {
                $statusOverview = Get-MgDeviceManagementDeviceConfigurationDeviceStatusOverview -DeviceConfigurationId $profile.Id -ErrorAction Stop
                $errorCount = $statusOverview.ErrorCount
                $conflictCount = $statusOverview.ConflictCount

                if ($errorCount -gt 0) { $findings += "ERRORS: $errorCount devices" }
                if ($conflictCount -gt 0) { $findings += "CONFLICTS: $conflictCount devices" }
            }
            catch { }

            $results.Add([PSCustomObject]@{
                PolicyName      = $profile.DisplayName
                PolicyType      = 'Configuration Profile'
                ProfileType     = $profileType
                Version         = $profile.Version
                Assignments     = $assignmentCount
                TargetTypes     = ($targetTypes | Select-Object -Unique) -join ', '
                LastModified    = $profile.LastModifiedDateTime
                Errors          = if ($errorCount) { $errorCount } else { 0 }
                Conflicts       = if ($conflictCount) { $conflictCount } else { 0 }
                Finding         = if ($findings.Count -gt 0) { $findings -join ' | ' } else { 'OK' }
            })
        }

        # --- Compliance Policies ---
        if ($IncludeCompliance) {
            Write-Verbose "Retrieving device compliance policies..."
            $compliancePolicies = Get-MgDeviceManagementDeviceCompliancePolicy -All -Property @(
                'id', 'displayName', '@odata.type', 'lastModifiedDateTime',
                'createdDateTime', 'version'
            )

            foreach ($policy in $compliancePolicies) {
                $findings = @()
                $policyOdataType = ($policy.AdditionalProperties.'@odata.type' ?? $policy.'@odata.type') -replace '#microsoft.graph.', ''

                try {
                    $assignments = Get-MgDeviceManagementDeviceCompliancePolicyAssignment -DeviceCompliancePolicyId $policy.Id -ErrorAction Stop
                    $assignmentCount = ($assignments | Measure-Object).Count
                }
                catch {
                    $assignments = @()
                    $assignmentCount = 0
                }

                if ($assignmentCount -eq 0) {
                    $findings += 'UNASSIGNED'
                }

                $targetTypes = @()
                foreach ($assignment in $assignments) {
                    $targetType = $assignment.Target.AdditionalProperties.'@odata.type' -replace '#microsoft.graph.', ''
                    $targetTypes += $targetType
                }

                try {
                    $statusOverview = Get-MgDeviceManagementDeviceCompliancePolicyDeviceStatusOverview -DeviceCompliancePolicyId $policy.Id -ErrorAction Stop
                    $errorCount = $statusOverview.ErrorCount
                    $conflictCount = $statusOverview.ConflictCount
                    $nonCompliantCount = $statusOverview.NonCompliantCount

                    if ($errorCount -gt 0) { $findings += "ERRORS: $errorCount" }
                    if ($conflictCount -gt 0) { $findings += "CONFLICTS: $conflictCount" }
                    if ($nonCompliantCount -gt 0) { $findings += "NON-COMPLIANT: $nonCompliantCount devices" }
                }
                catch {
                    $errorCount = 0
                    $conflictCount = 0
                    $nonCompliantCount = 0
                }

                $results.Add([PSCustomObject]@{
                    PolicyName      = $policy.DisplayName
                    PolicyType      = 'Compliance Policy'
                    ProfileType     = $policyOdataType
                    Version         = $policy.Version
                    Assignments     = $assignmentCount
                    TargetTypes     = ($targetTypes | Select-Object -Unique) -join ', '
                    LastModified    = $policy.LastModifiedDateTime
                    Errors          = $errorCount
                    Conflicts       = $conflictCount
                    Finding         = if ($findings.Count -gt 0) { $findings -join ' | ' } else { 'OK' }
                })
            }
        }
    }

    end {
        $flagged = $results | Where-Object { $_.Finding -ne 'OK' }
        Write-Host " Policies reviewed: $($results.Count) | Findings: $($flagged.Count)" -ForegroundColor Gray
        $results | Sort-Object PolicyType, Finding, PolicyName
    }
}