Checks/Azure/Test-IamCustomRoleHasPermissionToAdministerResourceLock.ps1

function Test-IamCustomRoleHasPermissionToAdministerResourceLock {
    <#
    .SYNOPSIS
        Tests if a custom role has permissions to administer resource locks.

    .DESCRIPTION
        Ensures a Custom Role is Assigned Permissions for Administering Resource Locks.
        Resource locks administration is a critical task that should be performed from
        a custom role with the appropriate permissions.

        This check examines custom roles for permissions like:
        - Microsoft.Authorization/locks/*
        - Microsoft.Authorization/locks/delete
        - Microsoft.Authorization/locks/write

        The check PASSES if a custom role exists with lock administration permissions
        (ensuring proper delegation exists).
        The check FAILS if no custom role has these permissions (indicating resource
        lock administration is not properly delegated).

    .PARAMETER CheckMetadata
        Hashtable containing check metadata from AzureChecks.json including:
        - id: Check identifier
        - severity: Severity level

    .OUTPUTS
        [PSCustomObject[]] Array of finding objects.

    .NOTES
        Data source: $script:IAMService[$subscriptionId].CustomRoles
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [hashtable]$CheckMetadata
    )

    $ErrorActionPreference = 'Stop'

    # Lock-related permissions to check for
    $lockPermissions = @(
        'Microsoft.Authorization/locks/*',
        'Microsoft.Authorization/locks/delete',
        'Microsoft.Authorization/locks/write',
        '*/locks/*',
        '*/locks/delete',
        '*/locks/write'
    )

    foreach ($subscriptionId in $script:IAMService.Keys) {
        $iamData = $script:IAMService[$subscriptionId]

        # Check if role definitions were loaded
        if (-not $iamData.RoleDefinitions) {
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'SKIPPED'
                StatusExtended = "Unable to retrieve role definitions for subscription $subscriptionId"
                ResourceId     = "/subscriptions/$subscriptionId"
                ResourceName   = "Subscription $subscriptionId"
            }
            New-CIEMFinding @findingParams
            continue
        }

        # Get custom roles for this subscription
        $customRoles = $iamData.CustomRoles

        if (-not $customRoles -or $customRoles.Count -eq 0) {
            # No custom roles exist - this is a FAIL condition
            # The recommendation is to have a dedicated custom role for lock administration
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'FAIL'
                StatusExtended = "No custom roles exist in subscription $subscriptionId. A dedicated custom role should be created for resource lock administration."
                ResourceId     = "/subscriptions/$subscriptionId"
                ResourceName   = "Subscription $subscriptionId"
            }
            New-CIEMFinding @findingParams
            continue
        }

        # Track if we found any custom role with lock permissions
        $foundLockAdminRole = $false
        $lockAdminRoles = @()

        foreach ($role in $customRoles) {
            # Strict mode safe property access
            $roleName = if ($role.properties.PSObject.Properties['roleName']) {
                $role.properties.roleName
            }
            else {
                'Unknown'
            }
            $roleId = $role.id
            $permissions = if ($role.properties.PSObject.Properties['permissions']) {
                $role.properties.permissions
            }
            else {
                $null
            }

            if (-not $permissions) {
                continue
            }

            # Check each permission set for lock-related actions
            foreach ($permSet in $permissions) {
                $actions = $permSet.actions
                if (-not $actions) {
                    continue
                }

                # Check if any lock permission is present
                $hasLockPermission = $false
                foreach ($action in $actions) {
                    # Check for exact matches or wildcard patterns
                    foreach ($lockPerm in $lockPermissions) {
                        if ($action -eq $lockPerm) {
                            $hasLockPermission = $true
                            break
                        }
                        # Check for wildcard match (e.g., * matches everything)
                        if ($action -eq '*') {
                            $hasLockPermission = $true
                            break
                        }
                        # Check for partial wildcard match
                        if ($lockPerm -like "$action*" -or $action -like "$lockPerm*") {
                            $hasLockPermission = $true
                            break
                        }
                    }
                    if ($hasLockPermission) {
                        break
                    }
                }

                if ($hasLockPermission) {
                    $foundLockAdminRole = $true
                    $lockAdminRoles += @{
                        Name = $roleName
                        Id   = $roleId
                    }
                    break
                }
            }
        }

        if ($foundLockAdminRole) {
            # Found custom role(s) with lock permissions - PASS
            $roleNames = ($lockAdminRoles | ForEach-Object { $_.Name }) -join ', '
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'PASS'
                StatusExtended = "Custom role(s) with resource lock administration permissions found in subscription $subscriptionId`: $roleNames"
                ResourceId     = "/subscriptions/$subscriptionId"
                ResourceName   = "Subscription $subscriptionId"
            }
            New-CIEMFinding @findingParams
        }
        else {
            # No custom role has lock permissions - FAIL
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'FAIL'
                StatusExtended = "No custom role with resource lock administration permissions found in subscription $subscriptionId. Custom roles exist ($($customRoles.Count)) but none have Microsoft.Authorization/locks/* permissions."
                ResourceId     = "/subscriptions/$subscriptionId"
                ResourceName   = "Subscription $subscriptionId"
            }
            New-CIEMFinding @findingParams
        }
    }
}