Assessment/ProductionTarget.ps1

# Production classification is shared between environment, service-connection and release rules.
# Explicit configuration is authoritative. Names are not production evidence.
function Test-AzDevOpsProductionName {
    [CmdletBinding()]
    param([AllowNull()][string]$Name)
    return $false
}

function Test-AzDevOpsProductionTarget {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject, [object]$Configuration)
    $type = [string]$TargetObject.ObjectType
    $name = [string]$TargetObject.ObjectName
    $configured = $Configuration.productionTargets
    if ($null -ne $configured) {
        $isConfigured = if ($configured -is [System.Collections.IDictionary]) {
            $configured.Contains($type)
        } else {
            $null -ne $configured.PSObject.Properties[$type]
        }
        if ($isConfigured) {
            $expected = @($configured.($type))
            return [pscustomobject]@{ Outcome = if ($name -cin $expected) { 'Production' } else { 'NonProduction' }; ReasonCode = 'ConfiguredProductionScope' }
        }
    }
    foreach ($propertyName in 'isProduction', 'production') {
        $property = $TargetObject.PSObject.Properties[$propertyName]
        if ($null -ne $property -and $property.Value -is [bool]) {
            return [pscustomobject]@{ Outcome = if ($property.Value) { 'Production' } else { 'NonProduction' }; ReasonCode = 'ApiProductionClassification' }
        }
    }
    return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'ProductionClassificationUnavailable' }
}

function Get-AzDevOpsProductionReleaseClassification {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject, [object]$Configuration)
    $release = [string]$TargetObject.ObjectName
    $configured = $Configuration.productionReleaseEnvironments
    $isConfigured = if ($null -eq $configured) { $false } elseif ($configured -is [System.Collections.IDictionary]) {
        $configured.Contains($release)
    } else {
        $null -ne $configured.PSObject.Properties[$release]
    }
    if ($isConfigured) {
        $expected = @($configured.($release))
        $matches = @($TargetObject.environments | Where-Object { $_.name -cin $expected })
        return [pscustomobject]@{ Outcome = if ($matches.Count -gt 0) { 'Production' } else { 'NonProduction' }; ReasonCode = 'ConfiguredProductionScope' }
    }
    $environments = @($TargetObject.environments)
    if ($environments.Count -eq 0) { return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'ProductionClassificationUnavailable' } }
    $values = @($environments | ForEach-Object {
        $property = $_.PSObject.Properties['isProduction']
        if ($null -ne $property -and $property.Value -is [bool]) { $property.Value } else { $null }
    })
    if ($values -contains $true) { return [pscustomobject]@{ Outcome = 'Production'; ReasonCode = 'ApiProductionClassification' } }
    if (@($values | Where-Object { $null -eq $_ }).Count -eq 0) { return [pscustomobject]@{ Outcome = 'NonProduction'; ReasonCode = 'ApiProductionClassification' } }
    return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'ProductionClassificationUnavailable' }
}

function Get-AzDevOpsProductionReleaseEnvironments {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject, [object]$Configuration)
    $classification = Get-AzDevOpsProductionReleaseClassification -TargetObject $TargetObject -Configuration $Configuration
    if ($classification.Outcome -ne 'Production') { return @() }
    $release = [string]$TargetObject.ObjectName
    $configured = $Configuration.productionReleaseEnvironments
    if ($null -ne $configured -and (($configured -is [System.Collections.IDictionary] -and $configured.Contains($release)) -or
        ($configured -isnot [System.Collections.IDictionary] -and $null -ne $configured.PSObject.Properties[$release]))) {
        return @($TargetObject.environments | Where-Object { $_.name -cin @($configured.($release)) })
    }
    return @($TargetObject.environments | Where-Object { $_.PSObject.Properties['isProduction'].Value -eq $true })
}

function Get-AzDevOpsProductionBranchAssessment {
    [CmdletBinding()]
    param([Parameter(Mandatory)][object]$TargetObject)
    if ($null -eq $TargetObject.Checks) {
        return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'ChecksUnavailable' }
    }
    $branchChecks = @($TargetObject.Checks | Where-Object {
        $_.type.name -eq 'Branch control' -or $_.settings.displayName -eq 'Branch control'
    })
    if ($branchChecks.Count -eq 0) {
        return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'MissingProductionBranchControl' }
    }
    $incomplete = $false
    foreach ($check in $branchChecks) {
        if ($check.isDisabled -eq $true) { continue }
        $allowed = $check.settings.inputs.allowedBranches
        if ($null -eq $allowed) { $incomplete = $true; continue }
        $names = @($allowed | ForEach-Object { [string]$_ -split '[,;]' } |
            ForEach-Object { $_.Trim() } | Where-Object { $_ })
        $unrestrictedOrInvalid = @($names | Where-Object {
            $_ -cnotmatch '^refs/heads/(?!\*+$)[^,;\s]+$'
        })
        if ($names.Count -gt 0 -and $unrestrictedOrInvalid.Count -eq 0) {
            return [pscustomobject]@{ Outcome = 'Pass'; ReasonCode = 'RestrictedProductionBranches' }
        }
    }
    if ($incomplete) {
        return [pscustomobject]@{ Outcome = 'Unknown'; ReasonCode = 'BranchCheckConfigurationUnavailable' }
    }
    return [pscustomobject]@{ Outcome = 'Fail'; ReasonCode = 'ProductionBranchRestrictionMissingOrDisabled' }
}