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() } |