Checks/Azure/Test-IamSubscriptionRolesOwnerCustomNotCreated.ps1

function Test-IamSubscriptionRolesOwnerCustomNotCreated {
    <#
    .SYNOPSIS
        Tests that no custom subscription owner roles are created.

    .DESCRIPTION
        Ensures that no custom subscription owner roles exist. Subscription ownership
        should not include permission to create custom owner roles. The principle of
        least privilege should be followed and only necessary privileges should be
        assigned instead of allowing full administrative access.

        A custom owner role is identified by:
        - Being a custom role (properties.type = 'CustomRole')
        - Having '*' (wildcard all) in the actions permissions
        - Having assignable scopes that include subscription level

        The check PASSES if no custom owner roles exist.
        The check FAILS if any custom role has owner-equivalent permissions.

    .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'

    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 - PASS (no custom owner roles can exist)
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'PASS'
                StatusExtended = "No custom roles exist in subscription $subscriptionId. No custom owner roles can exist."
                ResourceId     = "/subscriptions/$subscriptionId"
                ResourceName   = "Subscription $subscriptionId"
            }
            New-CIEMFinding @findingParams
            continue
        }

        # Track custom owner roles found
        $customOwnerRoles = @()

        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
            }
            $assignableScopes = if ($role.properties.PSObject.Properties['assignableScopes']) {
                $role.properties.assignableScopes
            }
            else {
                @()
            }

            if (-not $permissions) {
                continue
            }

            # Check if this role has owner-equivalent permissions
            $hasOwnerPermissions = $false

            foreach ($permSet in $permissions) {
                $actions = $permSet.actions
                $notActions = $permSet.notActions

                if (-not $actions) {
                    continue
                }

                # Check for wildcard (*) permission indicating full control
                $hasWildcard = $actions -contains '*'

                if ($hasWildcard) {
                    # Check if notActions significantly limits this
                    # A true owner role would have minimal or no notActions
                    $significantNotActions = $false
                    if ($notActions -and $notActions.Count -gt 0) {
                        # If notActions contains broad exclusions, this may not be owner-equivalent
                        # But for safety, we still flag roles with * permission
                        $significantNotActions = $notActions.Count -gt 10
                    }

                    # Check if assignable scopes include subscription level
                    $hasSubscriptionScope = $false
                    if ($assignableScopes) {
                        foreach ($scope in $assignableScopes) {
                            # Check for subscription-level or higher scope
                            if ($scope -match '^/subscriptions/[^/]+$' -or
                                $scope -eq '/' -or
                                $scope -match '^/providers/Microsoft\.Management/managementGroups/') {
                                $hasSubscriptionScope = $true
                                break
                            }
                        }
                    }

                    if ($hasSubscriptionScope -and -not $significantNotActions) {
                        $hasOwnerPermissions = $true
                        break
                    }
                }
            }

            if ($hasOwnerPermissions) {
                $customOwnerRoles += @{
                    Name            = $roleName
                    Id              = $roleId
                    AssignableScopes = $assignableScopes -join ', '
                }
            }
        }

        if ($customOwnerRoles.Count -gt 0) {
            # Found custom owner role(s) - generate a FAIL finding for each
            foreach ($ownerRole in $customOwnerRoles) {
                $findingParams = @{
                    CheckMetadata  = $CheckMetadata
                    Status         = 'FAIL'
                    StatusExtended = "Custom owner role '$($ownerRole.Name)' found with full permissions (*) at subscription scope. Custom roles should not have owner-equivalent permissions. Assignable scopes: $($ownerRole.AssignableScopes)"
                    ResourceId     = $ownerRole.Id
                    ResourceName   = $ownerRole.Name
                }
                New-CIEMFinding @findingParams
            }
        }
        else {
            # No custom owner roles found - PASS
            $findingParams = @{
                CheckMetadata  = $CheckMetadata
                Status         = 'PASS'
                StatusExtended = "No custom owner roles found in subscription $subscriptionId. $($customRoles.Count) custom role(s) exist but none have owner-equivalent permissions."
                ResourceId     = "/subscriptions/$subscriptionId"
                ResourceName   = "Subscription $subscriptionId"
            }
            New-CIEMFinding @findingParams
        }
    }
}