Public/Get-IntuneScopeTagAudit.ps1

function Get-IntuneScopeTagAudit {
    <#
    .SYNOPSIS
    Finds evidence-led Intune scope-tag conditions worth reviewing.
    .DESCRIPTION
    Examines role assignments, device configurations and mobile apps. Findings are
    observations, not compliance failures or vulnerability claims.
    .PARAMETER IncludeExtendedResources
    Also examines compliance policies, Settings Catalog and endpoint security policies,
    remediations and device health scripts. Connect with ExtendedScopeTagAudit first.
    .EXAMPLE
    Get-IntuneScopeTagAudit
    #>

    [CmdletBinding()]
    param(
        [switch] $IncludeExtendedResources
    )

    $requiredScopes = @(
        'DeviceManagementRBAC.Read.All',
        'GroupMember.Read.All',
        'DeviceManagementConfiguration.Read.All',
        'DeviceManagementApps.Read.All'
    )
    if ($IncludeExtendedResources) { $requiredScopes += 'DeviceManagementScripts.Read.All' }
    $null = Assert-IntuneAccessConnection -RequiredScope $requiredScopes

    $definitions = @(Get-IntuneAccessRoleDefinitions)
    $assignments = @(Get-IntuneAccessRoleAssignments -RoleDefinition $definitions -ResolveNames)
    $tags = @()
    $tagCollectionError = $null
    try {
        $tags = @(Get-IntuneAccessScopeTags)
    }
    catch {
        $tagCollectionError = $_.Exception.Message
    }
    $resources = @(Get-IntuneAccessAuditedResources -IncludeExtended:$IncludeExtendedResources)
    $findings = [System.Collections.Generic.List[object]]::new()
    $sequence = 0

    $assignmentTagIds = @($assignments | ForEach-Object { $_.RawIds.ScopeTagIds } | Select-Object -Unique)
    $resourceTagIds = @($resources | ForEach-Object ScopeTagIds | Select-Object -Unique)
    $customAssignmentTagIds = @($assignmentTagIds | Where-Object { $_ -ne '0' })

    if ($null -ne $tagCollectionError) {
        $sequence++
        $findings.Add([PSCustomObject] @{
            PSTypeName              = 'IntuneAccess.ScopeTagFinding'
            FindingId               = 'IA-ST-{0:d3}' -f $sequence
            Title                   = 'Scope tag collection unavailable'
            Severity                = 'Warning'
            ResourceType            = 'Audit data source'
            ResourceName            = 'Intune role scope tags'
            ResourceId              = $null
            ObservedState           = "The beta scope tag collection could not be read. $tagCollectionError"
            ExpectedOrComparedState = 'Assignment and resource tag IDs are retained where their enrichment calls succeeded.'
            Evidence                = @()
            Recommendation          = 'Check the current beta API contract and rerun the audit before acting on unresolved or unused-tag findings.'
            DocumentationUrl        = 'https://learn.microsoft.com/en-us/graph/api/intune-rbac-rolescopetag-list?view=graph-rest-beta'
        })
    }

    foreach ($assignment in $assignments | Where-Object { (Get-IntuneAccessProperty $_ 'ScopeTagDataState' 'Available') -eq 'Missing' }) {
        $sequence++
        $findings.Add([PSCustomObject] @{
            PSTypeName              = 'IntuneAccess.ScopeTagFinding'
            FindingId               = 'IA-ST-{0:d3}' -f $sequence
            Title                   = 'Assignment scope-tag data unavailable'
            Severity                = 'Review'
            ResourceType            = 'Role assignment'
            ResourceName            = $assignment.Name
            ResourceId              = $assignment.Id
            ObservedState           = 'The beta roleScopeTagIds property was missing from the Graph response.'
            ExpectedOrComparedState = 'No empty or all-tags value was inferred.'
            Evidence                = @("Assignment:$($assignment.Id)")
            Recommendation          = 'Recheck the beta API response and current Microsoft Graph documentation.'
            DocumentationUrl        = 'https://learn.microsoft.com/en-us/graph/api/resources/intune-rbac-deviceandappmanagementroleassignment?view=graph-rest-beta'
        })
    }

    foreach ($resource in $resources) {
        if ((Get-IntuneAccessProperty $resource 'ScopeTagDataState' 'Available') -eq 'Missing') {
            $sequence++
            $findings.Add([PSCustomObject] @{
                PSTypeName              = 'IntuneAccess.ScopeTagFinding'
                FindingId               = 'IA-ST-{0:d3}' -f $sequence
                Title                   = 'Resource scope-tag data unavailable'
                Severity                = 'Review'
                ResourceType            = $resource.ResourceType
                ResourceName            = $resource.ResourceName
                ResourceId              = $resource.ResourceId
                ObservedState           = 'The beta roleScopeTagIds property was missing from the Graph response.'
                ExpectedOrComparedState = 'The resource was not treated as Default-tagged.'
                Evidence                = @("Resource:$($resource.ResourceId)")
                Recommendation          = 'Recheck the beta API response before drawing a visibility conclusion.'
                DocumentationUrl        = 'https://learn.microsoft.com/en-us/intune/fundamentals/role-based-access-control/scope-tags'
            })
            continue
        }
        if ($resource.ScopeTagIds.Count -gt 1) {
            $sequence++
            $findings.Add([PSCustomObject] @{
                PSTypeName              = 'IntuneAccess.ScopeTagFinding'
                FindingId               = 'IA-ST-{0:d3}' -f $sequence
                Title                   = 'Several scope tags observed'
                Severity                = 'Information'
                ResourceType            = $resource.ResourceType
                ResourceName            = $resource.ResourceName
                ResourceId              = $resource.ResourceId
                ObservedState           = "Scope tag IDs: $($resource.ScopeTagIds -join ', ')"
                ExpectedOrComparedState = 'No organisational baseline supplied.'
                Evidence                = @($resource.ScopeTagIds)
                Recommendation          = 'Confirm that the combined visibility is intentional.'
                DocumentationUrl        = 'https://learn.microsoft.com/en-us/intune/fundamentals/role-based-access-control/scope-tags'
            })
        }

        if ($resource.ScopeTagIds.Count -eq 1 -and $resource.ScopeTagIds[0] -eq '0' -and $customAssignmentTagIds.Count -gt 0) {
            $sequence++
            $findings.Add([PSCustomObject] @{
                PSTypeName              = 'IntuneAccess.ScopeTagFinding'
                FindingId               = 'IA-ST-{0:d3}' -f $sequence
                Title                   = 'Potential visibility mismatch'
                Severity                = 'Review'
                ResourceType            = $resource.ResourceType
                ResourceName            = $resource.ResourceName
                ResourceId              = $resource.ResourceId
                ObservedState           = 'The resource uses only the Default scope tag.'
                ExpectedOrComparedState = "Observed role assignments reference custom tag IDs: $($customAssignmentTagIds -join ', ')."
                Evidence                = @("Resource:$($resource.ResourceId)", "Tags:$($resource.ScopeTagIds -join ',')")
                Recommendation          = 'Check whether administrators limited to custom scope tags are expected to see this resource.'
                DocumentationUrl        = 'https://learn.microsoft.com/en-us/intune/fundamentals/role-based-access-control/scope-tags'
            })
        }
    }

    foreach ($tagId in $assignmentTagIds) {
        if ($tagId -notin $resourceTagIds) {
            $tag = $tags | Where-Object Id -EQ $tagId | Select-Object -First 1
            $assignmentNames = @($assignments | Where-Object { $tagId -in $_.RawIds.ScopeTagIds } | ForEach-Object Name)
            $sequence++
            $findings.Add([PSCustomObject] @{
                PSTypeName              = 'IntuneAccess.ScopeTagFinding'
                FindingId               = 'IA-ST-{0:d3}' -f $sequence
                Title                   = 'Assignment scope tag not observed on examined resources'
                Severity                = 'Review'
                ResourceType            = 'Role assignment'
                ResourceName            = if ($null -ne $tag) { $tag.DisplayName } else { '[Unresolved scope tag]' }
                ResourceId              = $tagId
                ObservedState           = "Referenced by assignment(s): $($assignmentNames -join ', ')."
                ExpectedOrComparedState = 'No matching tag was found on the supported resources examined by this run.'
                Evidence                = @($assignmentNames)
                Recommendation          = 'Confirm whether the tag is intended for another supported Intune object type before changing anything.'
                DocumentationUrl        = 'https://learn.microsoft.com/en-us/intune/fundamentals/role-based-access-control/scope-tags'
            })
        }
    }

    foreach ($tag in $tags | Where-Object { -not $_.IsBuiltIn }) {
        if ($tag.Id -notin $assignmentTagIds -and $tag.Id -notin $resourceTagIds) {
            $sequence++
            $findings.Add([PSCustomObject] @{
                PSTypeName              = 'IntuneAccess.ScopeTagFinding'
                FindingId               = 'IA-ST-{0:d3}' -f $sequence
                Title                   = 'Custom scope tag appears unused in the examined data'
                Severity                = 'Information'
                ResourceType            = 'Scope tag'
                ResourceName            = $tag.DisplayName
                ResourceId              = $tag.Id
                ObservedState           = 'No reference was observed in role assignments or the supported resources examined by this run.'
                ExpectedOrComparedState = 'The audit does not examine every Intune object type.'
                Evidence                = @("ScopeTag:$($tag.Id)")
                Recommendation          = 'Review its purpose and check other Intune object types before considering removal.'
                DocumentationUrl        = 'https://learn.microsoft.com/en-us/intune/fundamentals/role-based-access-control/scope-tags'
            })
        }
    }

    if ($findings.Count -eq 0) {
        $findings.Add([PSCustomObject] @{
            PSTypeName              = 'IntuneAccess.ScopeTagFinding'
            FindingId               = 'IA-ST-000'
            Title                   = 'No review conditions found in supported object types'
            Severity                = 'Information'
            ResourceType            = 'Audit summary'
            ResourceName            = 'Scope tag audit'
            ResourceId              = $null
            ObservedState           = "Examined $($assignments.Count) role assignments and $($resources.Count) supported resources."
            ExpectedOrComparedState = if ($IncludeExtendedResources) { 'The extended supported resource set was examined.' } else { 'The base audit examines device configurations and mobile apps.' }
            Evidence                = @()
            Recommendation          = 'No action suggested.'
            DocumentationUrl        = 'https://learn.microsoft.com/en-us/intune/fundamentals/role-based-access-control/scope-tags'
        })
    }

    return $findings.ToArray()
}