Public/Test-CCBranchProtection.ps1

function Test-CCBranchProtection {
    <#
    .SYNOPSIS
        Validates branch protection rules on the default branch.

    .PARAMETER Repository
        GitHub repository in format "owner/repo". Auto-detected from git remote if not specified.

    .PARAMETER Token
        GitHub token. Falls back to GITHUB_TOKEN, GH_TOKEN, or gh CLI.

    .PARAMETER Standard
        Standard tier to check against: core, active, minimal. Default: core.

    .PARAMETER Config
        Path to per-repo config file for overrides.

    .OUTPUTS
        [PSCustomObject[]] Array of check results.

    .EXAMPLE
        Test-CCBranchProtection -Repository 'The-Code-Kitchen/PowerCraft.Secrets'
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter()]
        [string]$Repository,

        [Parameter()]
        [string]$Token,

        [Parameter()]
        [ValidateSet('core', 'active', 'minimal')]
        [string]$Standard = 'core',

        [Parameter()]
        [string]$Config
    )

    $results = [System.Collections.ArrayList]::new()
    $token = Resolve-CCToken -Token $Token
    if (-not $token) { throw "No GitHub token available. Set GITHUB_TOKEN or run 'gh auth login'." }

    $repo = Resolve-CCRepository -Repository $Repository
    if (-not $repo) { throw "Cannot determine repository. Specify -Repository 'owner/repo'." }

    $stdConfig = (Get-CCStandardConfig -Standard $Standard -ConfigPath $Config).branch_protection

    # Get repo info for default branch name
    $repoInfo = Invoke-CCGitHubApi -Endpoint "repos/$repo" -Token $token
    $defaultBranch = $repoInfo.default_branch

    # GH-BP-001: Default branch has protection rules
    $protection = Invoke-CCGitHubApi -Endpoint "repos/$repo/branches/$defaultBranch/protection" -Token $token -AllowNotFound

    if (-not $protection) {
        if ($stdConfig.require) {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-001' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Fail' -Severity 'Error' `
                        -Message "No branch protection rules on default branch ($defaultBranch)" `
                        -FixAvailable $true -FixId 'EnableBranchProtection' -Current $null -Expected 'Protection rules enabled'))
        }
        else {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-001' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Skipped' -Severity 'Info' `
                        -Message "Branch protection not required by '$Standard' standard"))
        }
        return $results.ToArray()
    }

    [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-001' -Category 'Branch Protection' -Item $defaultBranch `
                -Status 'Pass' -Severity 'Info' -Message "Branch protection enabled on $defaultBranch"))

    # GH-BP-002: Requires pull request reviews
    $prReviews = $protection.required_pull_request_reviews
    if ($stdConfig.min_reviewers -gt 0) {
        if ($prReviews) {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-002' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Pass' -Severity 'Info' -Message 'Pull request reviews required'))

            # GH-BP-003: Minimum reviewers
            $actualReviewers = $prReviews.required_approving_review_count
            if ($actualReviewers -ge $stdConfig.min_reviewers) {
                [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-003' -Category 'Branch Protection' -Item $defaultBranch `
                            -Status 'Pass' -Severity 'Info' `
                            -Message "Requires $actualReviewers approving review(s)" -Current $actualReviewers -Expected $stdConfig.min_reviewers))
            }
            else {
                [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-003' -Category 'Branch Protection' -Item $defaultBranch `
                            -Status 'Fail' -Severity 'Error' `
                            -Message "Requires $actualReviewers reviewer(s), minimum is $($stdConfig.min_reviewers)" `
                            -FixAvailable $true -FixId 'RequireReviews' -Current $actualReviewers -Expected $stdConfig.min_reviewers))
            }

            # GH-BP-004: Dismiss stale reviews
            if ($stdConfig.dismiss_stale_reviews) {
                if ($prReviews.dismiss_stale_reviews) {
                    [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-004' -Category 'Branch Protection' -Item $defaultBranch `
                                -Status 'Pass' -Severity 'Info' -Message 'Stale reviews dismissed on new push'))
                }
                else {
                    [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-004' -Category 'Branch Protection' -Item $defaultBranch `
                                -Status 'Fail' -Severity 'Warning' `
                                -Message 'Stale reviews not dismissed on new push' `
                                -FixAvailable $true -FixId 'DismissStaleReviews' -Current $false -Expected $true))
                }
            }
        }
        else {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-002' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Fail' -Severity 'Error' `
                        -Message 'Pull request reviews not required' `
                        -FixAvailable $true -FixId 'RequireReviews' -Current $null -Expected 'Reviews required'))
        }
    }

    # GH-BP-005: Requires status checks
    if ($stdConfig.require_status_checks) {
        $statusChecks = $protection.required_status_checks
        if ($statusChecks) {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-005' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Pass' -Severity 'Info' -Message "Status checks required: $($statusChecks.contexts -join ', ')"))

            # GH-BP-006: Up-to-date
            if ($stdConfig.require_up_to_date) {
                if ($statusChecks.strict) {
                    [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-006' -Category 'Branch Protection' -Item $defaultBranch `
                                -Status 'Pass' -Severity 'Info' -Message 'Branches must be up-to-date before merge'))
                }
                else {
                    [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-006' -Category 'Branch Protection' -Item $defaultBranch `
                                -Status 'Warning' -Severity 'Warning' `
                                -Message 'Branches not required to be up-to-date before merge' `
                                -Current $false -Expected $true))
                }
            }
        }
        else {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-005' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Fail' -Severity 'Error' `
                        -Message 'No status checks required before merge' `
                        -FixAvailable $true -FixId 'RequireStatusChecks' -Current $null -Expected 'Status checks required'))
        }
    }

    # GH-BP-007: Enforce admins
    if ($stdConfig.enforce_admins) {
        $enforced = $protection.enforce_admins.enabled
        $status = if ($enforced) { 'Pass' } else { 'Fail' }
        [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-007' -Category 'Branch Protection' -Item $defaultBranch `
                    -Status $status -Severity $(if ($enforced) { 'Info' } else { 'Warning' }) `
                    -Message $(if ($enforced) { 'Rules enforced for administrators' } else { 'Rules not enforced for administrators' }) `
                    -FixAvailable $true -FixId 'EnforceAdmins' -Current $enforced -Expected $true))
    }

    # GH-BP-008: Restrict force pushes
    if ($stdConfig.restrict_force_pushes) {
        $restricted = $protection.allow_force_pushes.enabled -eq $false
        if ($restricted) {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-008' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Pass' -Severity 'Info' -Message 'Force pushes restricted'))
        }
        else {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-008' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Fail' -Severity 'Error' `
                        -Message 'Force pushes allowed on default branch' `
                        -FixAvailable $true -FixId 'RestrictForcePush' -Current $true -Expected $false))
        }
    }

    # GH-BP-009: Restrict deletions
    if ($stdConfig.restrict_deletions) {
        $restricted = $protection.allow_deletions.enabled -eq $false
        if ($restricted) {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-009' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Pass' -Severity 'Info' -Message 'Branch deletion restricted'))
        }
        else {
            [void]$results.Add((New-CCCheckResult -CheckId 'GH-BP-009' -Category 'Branch Protection' -Item $defaultBranch `
                        -Status 'Fail' -Severity 'Warning' `
                        -Message 'Default branch can be deleted' `
                        -FixAvailable $true -FixId 'RestrictDeletion' -Current $true -Expected $false))
        }
    }

    return $results.ToArray()
}