Assessment/PolicyAssessment.ps1

function Get-AzDevOpsAssessmentField {
    param([object]$TargetObject, [string]$Field)
    $names = if ($TargetObject -is [System.Collections.IDictionary]) { @($TargetObject.Keys) } else { @($TargetObject.psobject.Properties.Name) }
    if ($Field -in $names) { return $TargetObject.$Field }
    $value = $TargetObject
    foreach ($part in $Field.Split('.')) {
        if ($null -eq $value) { return $null }
        $value = $value.$part
    }
    return $value
}

function Get-AzDevOpsAssessmentAgentImages {
    param([object]$TargetObject)
    if ($TargetObject.pool.vmImage) { [string]$TargetObject.pool.vmImage }
    foreach ($child in @($TargetObject.stages) + @($TargetObject.jobs)) {
        if ($null -ne $child) { Get-AzDevOpsAssessmentAgentImages -TargetObject $child }
    }
}

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

    $notApplicable = [pscustomobject]@{ Outcome = 'NotApplicable'; ReasonCode = 'PolicyNotRequired' }
    $unknown = [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'PolicyEvidenceUnavailable' }
    $pass = [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'PolicySatisfied' }
    $fail = [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'PolicyNotSatisfied' }
    # Public exposure remains restricted unless a referenced policy or public baseline approves it.
    if ($null -eq $Policy -and $RuleId -eq 'Azure.DevOps.Project.Visibility') {
        $Policy = @{ Reference = 'default-private-project'; Expected = 'private' }
    }
    if ($null -eq $Policy -and $RuleId -eq 'Azure.DevOps.Organization.Security.Policies.DisallowAnonymousAccess') {
        $Policy = @{ Reference = 'default-no-anonymous-access'; Expected = $false }
    }
    if ($null -eq $Policy) { return $notApplicable }
    if (-not $Policy.Reference) { return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'PolicyReferenceMissing' } }
    if ($null -ne $Policy.Targets -and $TargetObject.ObjectName -notin @($Policy.Targets)) {
        if ($RuleId -eq 'Azure.DevOps.Project.Visibility') { $Policy = @{ Reference = 'default-private-project'; Expected = 'private' } }
        elseif ($RuleId -eq 'Azure.DevOps.Organization.Security.Policies.DisallowAnonymousAccess') { $Policy = @{ Reference = 'default-no-anonymous-access'; Expected = $false } }
        else { return $notApplicable }
    }
    if ($Policy.Required -eq $false) { return $notApplicable }

    # Compensation is accepted only for the explicitly named control and reviewed evidence reference.
    if ($RuleId -match '(GitHubAdvancedSecurityEnabled|GitHubAdvancedSecurityBlockPushes|ProductionHumanApproval)$') {
        foreach ($control in $TargetObject.EquivalentControls) {
            if ($control.RuleId -eq $RuleId -and $control.Effective -eq $true -and
                $control.Reference -and $control.Reference -in @($Policy.ApprovedEquivalentControls)) {
                return [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'ApprovedEquivalentControl' }
            }
        }
        if ($RuleId -match 'GitHubAdvancedSecurity' -and $Policy.Licensed -eq $false) { return $notApplicable }
    }

    if ($RuleId -like '*.ProductionHumanApproval') {
        if ($null -eq $TargetObject.Checks) { return $unknown }
        $approvals = @($TargetObject.Checks | Where-Object { $_.type.name -eq 'Approval' -and $_.isDisabled -ne $true })
        if ($approvals.Count -eq 0) { return $fail }
        $incomplete = $false
        foreach ($approval in $approvals) {
            if ($null -eq $approval.settings.approvers) { $incomplete = $true; continue }
            if (@($approval.settings.approvers).Count -gt 0) { return $pass }
        }
        if ($incomplete) { return $unknown }
        return $fail
    }
    if ($RuleId -eq 'Azure.DevOps.Pipelines.PipelineYaml.AgentPoolVersionNotLatest') {
        if ($null -eq $Policy.AllowedImages -or @($Policy.AllowedImages).Count -eq 0) { return $unknown }
        $images = @(Get-AzDevOpsAssessmentAgentImages -TargetObject $TargetObject)
        if ($images.Count -eq 0) { return $unknown }
        $incomplete = $false
        foreach ($image in $images) {
            if ($image -match '\$\(|\$\{\{') { $incomplete = $true; continue }
            if ($image -notin @($Policy.AllowedImages) -or $image -in @($Policy.RetiredImages)) { return $fail }
        }
        if ($incomplete) { return $unknown }
        return $pass
    }

    $fields = @{
        'Azure.DevOps.Organization.GeneralOverview.TimeZoneSet' = 'timeZone'
        'Azure.DevOps.Organization.GeneralOverview.GeographySet' = 'geography'
        'Azure.DevOps.Organization.GeneralBillingSettings.AssignmentBillingEnabled' = 'isAssignmentBillingEnabled'
        'Azure.DevOps.Organization.GeneralBillingSettings.EnterpriseBillingConfigured' = 'isEnterpriseBillingEnabled'
        'Azure.DevOps.Repos.License' = 'LicenseExists'
        'Azure.DevOps.Repos.GitHubAdvancedSecurityEnabled' = 'Ghas.advSecEnabled'
        'Azure.DevOps.Repos.GitHubAdvancedSecurityBlockPushes' = 'Ghas.blockPushes'
        'Azure.DevOps.Organization.Security.Policies.DisallowSecureShell' = 'policy.disallowSecureShell'
        'Azure.DevOps.Organization.Security.Policies.DisallowAadGuestUserAccess' = 'policy.disallowAadGuestUserAccess'
        'Azure.DevOps.Organization.Security.Policies.DisallowOAuthAuthentication' = 'policy.disallowOAuthAuthentication'
        'Azure.DevOps.Organization.Security.Policies.DisallowAnonymousAccess' = 'policy.allowAnonymousAccess'
        'Azure.DevOps.Project.Visibility' = 'visibility'
        'Azure.DevOps.RetentionSettings.ArtifactMinimumRetentionDays' = 'RetentionSettings.purgeArtifacts.value'
        'Azure.DevOps.RetentionSettings.PullRequestRunsMinimumRetentionDays' = 'RetentionSettings.purgePullRequestRuns.value'
    }
    if (-not $fields.ContainsKey($RuleId)) { return $unknown }
    $value = Get-AzDevOpsAssessmentField -TargetObject $TargetObject -Field $fields[$RuleId]
    if ($null -eq $value) { return $unknown }
    if ($RuleId -like 'Azure.DevOps.RetentionSettings.*') {
        [double]$minimum = 0; [double]$actual = 0
        if ($null -eq $Policy.Minimum -or -not [double]::TryParse([string]$Policy.Minimum, [ref]$minimum) -or
            -not [double]::TryParse([string]$value, [ref]$actual) -or $minimum -lt 0 -or $actual -lt 0) { return $unknown }
        if ($actual -ge $minimum) { return $pass }
        return $fail
    }
    $expected = $Policy.Expected
    if ($null -eq $expected -and $RuleId -match '^Azure.DevOps.Repos.(License|GitHubAdvancedSecurityEnabled|GitHubAdvancedSecurityBlockPushes)$') { $expected = $true }
    if ($null -eq $expected) { return $unknown }
    if ($value -in @($expected)) {
        if ($RuleId -eq 'Azure.DevOps.Organization.Security.Policies.DisallowAadGuestUserAccess' -and $value -eq $false) {
            $evidence = $TargetObject.GuestAccessEvidence
            if ($null -eq $evidence -or $evidence.Status -ne 'Complete' -or -not $evidence.Reference -or
                $evidence.Reference -notin @($Policy.ApprovedGuestReviews) -or $null -eq $evidence.Guests -or $null -eq $Policy.ApprovedGuests) {
                return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'GuestGovernanceEvidenceUnavailable' }
            }
            foreach ($guest in $evidence.Guests) {
                if (-not $guest.Descriptor) { return $unknown }
                if ($guest.Descriptor -notin @($Policy.ApprovedGuests)) {
                    return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'UnapprovedGuestAccess' }
                }
            }
        }
        return $pass
    }
    return $fail
}