Public/Test-PatPolicy.ps1
|
function Test-PatPolicy { [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory)] [ValidatePattern('^[a-zA-Z0-9._-]+$')] [string]$Owner, [Parameter(Mandatory)] [string]$Token ) $resource = "org/$Owner" $results = [System.Collections.Generic.List[PSCustomObject]]::new() $ownerContext = Get-FylgyrOwnerContext -Owner $Owner -Token $Token if ($ownerContext.Type -eq 'User') { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Info' ` -Severity 'Info' ` -Resource $resource ` -Detail "Owner '$Owner' is a personal account. Organization PAT policy does not apply." ` -Remediation 'No action needed. Run this check against an organization owner.' ` -Target $resource)) return $results.ToArray() } $requestsAvailable = $false $tokensAvailable = $false $tokensUnavailableReason = '' $requestCount = 0 try { $patRequests = Invoke-GitHubApi -Endpoint "orgs/$Owner/personal-access-token-requests?per_page=100" -Token $Token $requestsAvailable = $true if ($patRequests -is [System.Array]) { $requestCount = $patRequests.Count } elseif ($patRequests -and $patRequests.PSObject.Properties['requests']) { $requestCount = @($patRequests.requests).Count } else { $requestCount = @($patRequests).Count } } catch { $msg = $_.Exception.Message if ($msg -match '403') { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Info' ` -Severity 'Info' ` -Resource $resource ` -Detail 'Unable to evaluate organization PAT policy endpoint. Access may be blocked by token permissions, organization plan/feature gating, or endpoint token-type restrictions.' ` -Remediation 'Verify endpoint availability for your organization plan and, when required, use a GitHub App user/installation token with Personal access token requests/read and Personal access tokens/read organization permissions.' ` -Target $resource)) return $results.ToArray() } if ($msg -match '404') { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Info' ` -Severity 'Info' ` -Resource $resource ` -Detail 'Organization PAT policy endpoint is not available for this organization plan/feature context.' ` -Remediation 'Use available access-control features and enforce short PAT expirations. If PAT policy verification is required, verify endpoint availability and token-type requirements in GitHub REST docs.' ` -Target $resource)) return $results.ToArray() } $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Error' ` -Severity 'High' ` -Resource $resource ` -Detail "Failed to evaluate personal access token request policy: $($_.Exception.Message)" ` -Remediation 'Verify token scope and organization access, then rerun.' ` -Target $resource)) return $results.ToArray() } try { $null = Invoke-GitHubApi -Endpoint "orgs/$Owner/personal-access-tokens?per_page=1" -Token $Token $tokensAvailable = $true } catch { $msg = $_.Exception.Message if ($msg -match '403') { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Info' ` -Severity 'Info' ` -Resource $resource ` -Detail 'Token cannot read active fine-grained PAT records. PAT policy analysis is partial (permission, plan/feature gating, or token-type restriction).' ` -Remediation 'Verify endpoint availability for your organization plan and, when required, use a GitHub App user/installation token with Personal access tokens/read organization permission.' ` -Target $resource)) return $results.ToArray() } if ($msg -match '404') { $tokensUnavailableReason = '404' } else { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Error' ` -Severity 'High' ` -Resource $resource ` -Detail "Failed to evaluate active PAT records: $($_.Exception.Message)" ` -Remediation 'Verify token scope and organization access, then rerun.' ` -Target $resource)) return $results.ToArray() } } if ($requestsAvailable -and -not $tokensAvailable -and $tokensUnavailableReason -eq '404') { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Info' ` -Severity 'Info' ` -Resource $resource ` -Detail 'PAT request endpoint is reachable, but active PAT records endpoint is unavailable (404) in this org context. Governance verification is partial.' ` -Remediation 'Treat this check as advisory in this org context. Confirm PAT governance in organization settings and verify token-type requirements for PAT policy endpoints in GitHub docs.' ` -Target $resource)) return $results.ToArray() } if ($requestsAvailable -and $tokensAvailable -and $requestCount -gt 0) { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Pass' ` -Severity 'Info' ` -Resource $resource ` -Detail "PAT policy endpoints are active and $requestCount fine-grained PAT request record(s) were observed." ` -Remediation 'No action needed. Keep requiring PAT approval and continue reducing classic PAT usage.' ` -Target $resource)) } elseif ($requestsAvailable -and $tokensAvailable) { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Warning' ` -Severity 'High' ` -Resource $resource ` -Detail 'PAT governance endpoints are reachable, but no request records were observed. Fine-grained PAT approval enforcement could not be confirmed from API evidence alone.' ` -Remediation 'Review organization PAT settings: require approval for fine-grained PATs and restrict classic PAT access where possible.' ` -AttackMapping @('uber-credential-leak', 'github-device-code-phishing') ` -Target $resource)) } else { $results.Add((Format-FylgyrResult ` -CheckName 'PatPolicy' ` -Status 'Fail' ` -Severity 'High' ` -Resource $resource ` -Detail 'Could not verify organization PAT governance. Unrestricted or long-lived tokens increase the blast radius of endpoint compromise and phishing attacks.' ` -Remediation 'Enable fine-grained PAT approval workflow, restrict classic PAT access, and enforce short token expiration policies.' ` -AttackMapping @('uber-credential-leak', 'github-device-code-phishing') ` -Target $resource)) } $results.ToArray() } |