Assessment/ServiceConnectionScope.ps1

# Verifies cloud-side RBAC evidence for the identity behind an Azure service connection.
# A DevOps scopeLevel alone is not sufficient to infer effective Azure permissions.
function Get-AzDevOpsServiceConnectionScopeAssessment {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject, [object]$Policy)

    $unknown = [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'CloudRbacEvidenceUnavailable' }
    if ($null -eq $Policy -or -not $Policy.Reference -or $null -eq $Policy.ApprovedAssignments) {
        return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'CloudRbacPolicyUnavailable' }
    }
    $evidence = $TargetObject.CloudRbacEvidence
    if ($null -eq $evidence -or $evidence.Status -ne 'Complete' -or
        $evidence.InheritedAssignmentsIncluded -ne $true -or
        $evidence.RoleDefinitionsComplete -ne $true -or
        -not $evidence.Reference -or -not $evidence.PrincipalId -or
        ($evidence.IdentityVerified -isnot [bool] -or $evidence.IdentityVerified -ne $true) -or
        [string]$evidence.TargetName -cne [string]$TargetObject.ObjectName -or
        $null -eq $evidence.RoleAssignments) {
        return $unknown
    }
    $applicationId = [string]$TargetObject.authorization.parameters.serviceprincipalid
    $tenantId = [string]$TargetObject.authorization.parameters.tenantid
    $mapping = $evidence.IdentityMapping
    if ($null -eq $mapping -or $mapping.Verified -isnot [bool] -or
        $mapping.Verified -ne $true -or
        [string]::IsNullOrWhiteSpace([string]$mapping.Reference)) {
        return $unknown
    }
    foreach ($identity in @(
        $applicationId, $tenantId, [string]$evidence.PrincipalId,
        [string]$mapping.ApplicationId, [string]$mapping.PrincipalId, [string]$mapping.TenantId
    )) {
        $parsedIdentity = [guid]::Empty
        if (-not [guid]::TryParse($identity, [ref]$parsedIdentity) -or $parsedIdentity -eq [guid]::Empty) {
            return $unknown
        }
    }
    if ([string]$mapping.ApplicationId -ine $applicationId -or
        [string]$mapping.PrincipalId -ine [string]$evidence.PrincipalId -or
        [string]$mapping.TenantId -ine $tenantId) {
        return $unknown
    }
    $allowed = @($Policy.ApprovedAssignments)
    foreach ($assignment in @($evidence.RoleAssignments)) {
        if (-not $assignment.Scope -or -not $assignment.RoleDefinitionId) { return $unknown }
        $scope = ([string]$assignment.Scope).TrimEnd('/')
        $role = [string]$assignment.RoleDefinitionId
        $approved = @($allowed | Where-Object {
            $_.Scope -and $_.RoleDefinitionId -and
            ([string]$_.Scope).TrimEnd('/') -ieq $scope -and
            [string]$_.RoleDefinitionId -ieq $role
        }).Count -gt 0
        if (-not $approved) {
            return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'UnapprovedCloudRbacAssignment' }
        }
    }
    return [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'CloudRbacAssignmentsApproved' }
}