Assessment/ProjectValidUsersAssessment.ps1

# Check direct sensitive grants to Project Valid Users using namespace-specific policy.
# This is not a substitute for full effective permissions (nested groups, bypass, inherited ACL paths).
function Get-AzDevOpsProjectValidUsersAcls {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject, [Parameter(Mandatory)][string]$RuleId)
    $groups = @{
        'Azure.DevOps.Project.MainPipelineAcl.ProjectValidUsers' = 'Pipelines'
        'Azure.DevOps.Project.MainServiceConnectionAcl.ProjectValidUsers' = 'ServiceConnections'
        'Azure.DevOps.Project.MainRepositoryAcl.ProjectValidUsers' = 'Repositories'
        'Azure.DevOps.Project.MainEnvironmentAcl.ProjectValidUsers' = 'Environments'
        'Azure.DevOps.Project.MainReleaseDefinitionAcl.ProjectValidUsers' = 'ReleaseDefinitions'
        'Azure.DevOps.Project.MainVariableGroupAcl.ProjectValidUsers' = 'VariableGroups'
    }
    if ($groups.ContainsKey($RuleId)) {
        $field = $groups[$RuleId]
        return $TargetObject.ProjectAcls.($field)
    }
    return $TargetObject.Acls
}

function Get-AzDevOpsProjectValidUsersAssessment {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject, [Parameter(Mandatory)][string]$RuleId, [object]$Policy)

    $unknown = [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'BroadGroupPermissionEvidenceUnavailable' }
    if ($null -eq $Policy -or -not $Policy.Reference -or $null -eq $Policy.SensitiveMask) {
        return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'BroadGroupPermissionPolicyUnavailable' }
    }
    [long]$sensitive = 0
    [long]$approved = 0
    if (-not [long]::TryParse([string]$Policy.SensitiveMask, [ref]$sensitive) -or $sensitive -le 0) {
        return $unknown
    }
    if ($null -ne $Policy.ApprovedMask -and (-not [long]::TryParse([string]$Policy.ApprovedMask, [ref]$approved) -or $approved -lt 0)) {
        return $unknown
    }
    $acls = @(Get-AzDevOpsProjectValidUsersAcls -TargetObject $TargetObject -RuleId $RuleId)
    if ($acls.Count -eq 0 -or ($acls.Count -eq 1 -and $null -eq $acls[0])) { return $unknown }
    $incomplete = $false
    $violations = 0
    foreach ($acl in $acls) {
        if ($null -eq $acl -or $null -eq $acl.acesDictionary) { $incomplete = $true; continue }
        $aces = if ($acl.acesDictionary -is [System.Collections.IDictionary]) {
            @($acl.acesDictionary.Values)
        } else {
            @($acl.acesDictionary.PSObject.Properties.Value)
        }
        foreach ($ace in $aces) {
            if ($null -eq $ace -or -not $ace.descriptor) { $incomplete = $true; continue }
            if ([string]$ace.descriptor -notmatch '-0-0-0-0-3$') { continue }
            [long]$allow = 0
            [long]$deny = 0
            if (-not [long]::TryParse([string]$ace.allow, [ref]$allow) -or
                -not [long]::TryParse([string]$ace.deny, [ref]$deny) -or
                $allow -lt 0 -or $deny -lt 0) {
                $incomplete = $true; continue
            }
            $unapproved = ($allow -band (-bnot $deny)) -band $sensitive -band (-bnot $approved)
            if ($unapproved -ne 0) { $violations++ }
        }
    }
    if ($violations -gt 0) {
        return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'UnauthorizedProjectValidUsersDirectGrant' }
    }
    if ($incomplete) { return $unknown }
    return [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'ProjectValidUsersSensitiveDirectGrantsAuthorized' }
}