public/core/Connect-MtGitHub.ps1
|
function Connect-MtGitHub { <# .SYNOPSIS Establishes a GitHub Enterprise Cloud organization REST API session for Maester. .DESCRIPTION Establishes a GitHub Enterprise Cloud organization REST API session for Maester CIS benchmark tests. This is an organization-scoped session, not a full enterprise-admin session: enterprise-admin endpoints under /enterprises/{enterprise} are not verified by this command and would require a future enterprise-access probe if CIS controls need them. If no explicit token is provided, Maester signs in with the Maester CLI GitHub App using GitHub device flow. If the app is not installed or approved for the requested organization, Maester asks before opening the app install page and retries the org access probes after the user completes the GitHub install/approval step. If the user declines, Maester prints token-based auth guidance instead. It then validates the resulting token via four probes. The first three are blocking (any failure aborts the connection); the fourth is non-blocking (failure emits a warning but the session is still established): 1. GET /user — token identity (blocking) 2. GET /orgs/{org} — org metadata (blocking) 3. GET /user/memberships/orgs/{org} — signed-in user's org membership (blocking) 4. GET /orgs/{org}/actions/permissions — administration access probe (non-blocking) The membership probe is the real access gate: /orgs/{org} returns public metadata even for tokens with no relationship to the organization. Probe 3 intentionally uses /user/memberships/orgs/{org} because it verifies the signed-in user's own membership without requiring permission to read another user's membership record. A 4xx, malformed body, or missing state/role aborts with FailureReason = 'OrgMembershipFailed'. A successful response whose membership state is anything other than 'active' (e.g. 'pending' for an invitation that has not been accepted) also aborts the connection, with FailureReason = 'OrgMembershipPending' — only an active membership proves the user can act on the organization. Probe 4 verifies the token can reach an org-administration endpoint. GitHub documents /orgs/{org}/actions/permissions as requiring classic PAT 'admin:org' or fine-grained 'Organization Administration: read'. Failure here records AdministrationPermissionVerified = $false and emits a warning, but does not flip Connected to $false — the session remains usable for controls that don't require org administration access. On success, Role = 'admin' + RoleState = 'active' + AdministrationPermissionVerified = $true is the no-warning path. Other active roles (member, billing_manager, etc.) or admin-probe failure still connect but emit warnings indicating limited CIS coverage. Non-active membership states do not connect at all. Token resolution order: 1. -Token parameter (SecureString) 2. MAESTER_GITHUB_TOKEN environment variable 3. GH_TOKEN environment variable (GitHub CLI convention) 4. Maester CLI GitHub App device flow (interactive sessions) Required permissions: Maester GitHub App: Organization Members: read + Organization Administration: read Classic PAT: admin:org Fine-grained PAT: Organization Members: read + Organization Administration: read Required role for full coverage: organization owner/admin Note: Connection success proves token validity and org access, not that all CIS control fields will be visible. Each CIS test validates field availability and skips with an informative message if required fields are absent. .PARAMETER Organization GitHub organization login name. Falls back to GitHubOrganization in maester-config.json. .PARAMETER Token GitHub token as SecureString. Falls back to MAESTER_GITHUB_TOKEN, GH_TOKEN, or the Maester CLI GitHub App device flow. .PARAMETER ApiBaseUri GitHub API base URI. Falls back to GitHubApiBaseUri config, then https://api.github.com. Set to https://api.{subdomain}.ghe.com for GHE.com EMU deployments. .PARAMETER ApiVersion GitHub REST API version date. Falls back to GitHubApiVersion config, then 2022-11-28. GitHub defaults requests without X-GitHub-Api-Version to 2022-11-28. .EXAMPLE Connect-MtGitHub -Organization 'mycompany' .EXAMPLE Connect-MtGitHub -Organization 'mycompany' -ApiBaseUri 'https://api.myco.ghe.com' .LINK https://maester.dev/docs/commands/Connect-MtGitHub #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Consistent with other Connect-* functions')] [CmdletBinding()] param( [Parameter(Mandatory = $false)] [string] $Organization, [Parameter(Mandatory = $false)] [securestring] $Token, [Parameter(Mandatory = $false)] [string] $ApiBaseUri, [Parameter(Mandatory = $false)] [string] $ApiVersion ) # Clear prior GitHub session state (prevents stale data on reconnect) $__MtSession.GitHubConnection = $null $__MtSession.GitHubAuthHeader = $null $__MtSession.GitHubCache = @{} # Lazy-load config once if MaesterConfig is not yet set and any config-backed parameter is # omitted. This makes the config fallback work when Connect-MtGitHub is called before # Invoke-Maester (the normal pre-run workflow). Get-MtMaesterConfig walks up to 5 parent # directories from the given path, so it finds the config from anywhere in the test tree. if ($null -eq $__MtSession.MaesterConfig -and ( [string]::IsNullOrWhiteSpace($Organization) -or [string]::IsNullOrWhiteSpace($ApiBaseUri) -or [string]::IsNullOrWhiteSpace($ApiVersion))) { $__MtSession.MaesterConfig = Get-MtMaesterConfig -Path (Get-Location).Path } # Resolve organization (param -> config -> error) if ([string]::IsNullOrWhiteSpace($Organization)) { $Organization = Get-MtMaesterConfigGlobalSetting -SettingName 'GitHubOrganization' } if ([string]::IsNullOrWhiteSpace($Organization)) { Write-Host "`nNo GitHub organization specified. Provide -Organization or set GitHubOrganization in maester-config.json." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'NotConfigured' } return } # Surrounding whitespace from a config file or shell-quoted parameter would otherwise # be URL-encoded into the probe paths and silently 404 against GitHub. $Organization = $Organization.Trim() # Resolve ApiBaseUri (param -> config -> default). $resolvedApiBaseUri = $ApiBaseUri if ([string]::IsNullOrWhiteSpace($resolvedApiBaseUri)) { $configApiBaseUri = Get-MtMaesterConfigGlobalSetting -SettingName 'GitHubApiBaseUri' if (-not [string]::IsNullOrWhiteSpace($configApiBaseUri)) { $resolvedApiBaseUri = $configApiBaseUri } } if ([string]::IsNullOrWhiteSpace($resolvedApiBaseUri)) { $resolvedApiBaseUri = 'https://api.github.com' } $resolvedApiBaseUri = $resolvedApiBaseUri.Trim().TrimEnd('/') # Validate the fully-resolved URI. Done in-body (not via parameter [ValidatePattern]) so # config-supplied values are checked too, and so an invalid value records InvalidApiBaseUri # after the session-clearing logic at the top of this function rather than throwing before it. # Host allowlist prevents the GitHub token from being sent to arbitrary HTTPS hosts: only the # documented GitHub API endpoints — api.github.com (SaaS) and api.<subdomain>.ghe.com # (GHE.com data residency) — are accepted. GHES on-prem is intentionally not supported. $parsedUri = $null if (-not [uri]::TryCreate($resolvedApiBaseUri, [UriKind]::Absolute, [ref]$parsedUri) -or $parsedUri.Scheme -cne 'https') { Write-Host "`nGitHub API base URI must be an absolute https:// URI. Got: '$resolvedApiBaseUri'." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'InvalidApiBaseUri' } return } $uriHost = $parsedUri.Host.ToLowerInvariant() $isGitHubSaas = $uriHost -eq 'api.github.com' $isGheCom = $uriHost -match '^api\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.ghe\.com$' $hasRootPath = [string]::IsNullOrEmpty($parsedUri.AbsolutePath) -or $parsedUri.AbsolutePath -eq '/' # Reject query strings, fragments, and non-default ports — none are valid for the # GitHub API base, and accepting them would let an attacker-supplied config redirect # the GitHub token to an unexpected listener (e.g. a debug proxy on :8443) while still passing # the host allowlist. $hasNoQuery = [string]::IsNullOrEmpty($parsedUri.Query) $hasNoFragment = [string]::IsNullOrEmpty($parsedUri.Fragment) $hasDefaultPort = $parsedUri.IsDefaultPort if (-not (($isGitHubSaas -or $isGheCom) -and $hasRootPath -and $hasNoQuery -and $hasNoFragment -and $hasDefaultPort)) { Write-Host "`nGitHub API base URI must be https://api.github.com or https://api.<subdomain>.ghe.com (no path, query, fragment, or non-default port). Got: '$resolvedApiBaseUri'." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'InvalidApiBaseUri' } return } $ApiBaseUri = $resolvedApiBaseUri # Resolve ApiVersion (param -> config -> default). Validation runs on the resolved value # so the same FailureReason path applies whether the bad value came from -ApiVersion or # GitHubApiVersion config — a parameter [ValidatePattern] would otherwise throw before # the session-clearing logic at the top of this function ran. $resolvedApiVersion = $ApiVersion if ([string]::IsNullOrWhiteSpace($resolvedApiVersion)) { $configApiVersion = Get-MtMaesterConfigGlobalSetting -SettingName 'GitHubApiVersion' if (-not [string]::IsNullOrWhiteSpace($configApiVersion)) { $resolvedApiVersion = $configApiVersion } } if ([string]::IsNullOrWhiteSpace($resolvedApiVersion)) { $resolvedApiVersion = '2022-11-28' } $resolvedApiVersion = $resolvedApiVersion.Trim() if ($resolvedApiVersion -notmatch '^\d{4}-\d{2}-\d{2}$') { Write-Host "`nGitHub API version must use YYYY-MM-DD format. Got: '$resolvedApiVersion'." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'InvalidApiVersion' } return } $ApiVersion = $resolvedApiVersion # Resolve token (param -> MAESTER_GITHUB_TOKEN -> GH_TOKEN -> Maester GitHub App device flow) $maesterGitHubAppClientId = 'Iv23liV3mw0hSq0gn957' $maesterGitHubAppInstallUrl = 'https://github.com/apps/maester-cli/installations/new' $plainToken = $null $authenticationType = $null $tokenExpiresAt = $null $bstr = [IntPtr]::Zero try { if ($Token) { $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Token) $plainToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) $authenticationType = 'TokenParameter' } elseif (-not [string]::IsNullOrEmpty($env:MAESTER_GITHUB_TOKEN)) { $plainToken = $env:MAESTER_GITHUB_TOKEN $authenticationType = 'MAESTER_GITHUB_TOKEN' } elseif (-not [string]::IsNullOrEmpty($env:GH_TOKEN)) { $plainToken = $env:GH_TOKEN $authenticationType = 'GH_TOKEN' } } finally { if ($bstr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } if ([string]::IsNullOrEmpty($plainToken) -and (Get-MtUserInteractive)) { $deviceToken = Get-MtGitHubAppDeviceToken -ClientId $maesterGitHubAppClientId if ($null -eq $deviceToken -or [string]::IsNullOrWhiteSpace([string]$deviceToken.AccessToken)) { $failureReason = if ($null -ne $deviceToken -and -not [string]::IsNullOrWhiteSpace([string]$deviceToken.FailureReason)) { [string]$deviceToken.FailureReason } else { 'GitHubDeviceFlowFailed' } $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = $failureReason } return } $plainToken = [string]$deviceToken.AccessToken $authenticationType = 'GitHubAppDeviceFlow' if ($deviceToken.PSObject.Properties.Name -contains 'ExpiresAt') { $tokenExpiresAt = $deviceToken.ExpiresAt } } if ([string]::IsNullOrEmpty($plainToken)) { Write-Host "`nNo GitHub token found. Run Connect-MtGitHub in an interactive session to sign in with the Maester GitHub App, or provide -Token / MAESTER_GITHUB_TOKEN / GH_TOKEN for automation." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'NoToken' } return } # Auth headers — token stored here, NOT in the connection object $authHeaders = @{ Authorization = "Bearer $plainToken" Accept = 'application/vnd.github+json' 'X-GitHub-Api-Version' = $ApiVersion 'User-Agent' = 'Maester-GitHubCis' } Remove-Variable -Name plainToken -ErrorAction SilentlyContinue # Probe 1: token identity try { $userResponse = Invoke-WebRequest -Uri "$ApiBaseUri/user" -Headers $authHeaders -Method GET -UseBasicParsing -ErrorAction Stop $user = $userResponse.Content | ConvertFrom-Json Write-Verbose "GitHub token identity: $($user.login)" } catch { $rateLimitMessage = Get-MtGitHubRateLimitMessage -ErrorRecord $_ if ($rateLimitMessage) { Write-Host "`n$rateLimitMessage" -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'RateLimited' } return } $code = Get-MtGitHubErrorStatusCode -ErrorRecord $_ $apiMsg = Get-MtGitHubErrorMessage -ErrorRecord $_ # 410 = unsupported API version. 400 with a message about API/version support # also indicates an unsupported X-GitHub-Api-Version header value — GitHub's # documented wording includes "Not a supported version" and "version is not supported". $isUnsupportedApiVersion = $code -eq 410 -or ($code -eq 400 -and $apiMsg -match '(?i)api\s+version|x-github-api-version|not\s+.*supported.*version|version.*not\s+.*supported') if ($isUnsupportedApiVersion) { Write-Host "`nGitHub API version '$ApiVersion' is not supported by GitHub. Update GitHubApiVersion or omit it to use the default." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'InvalidApiVersion' } return } # $null status code means no HTTP response was produced (DNS failure, TLS handshake # failure, connection refused, hostname unreachable). The GitHub token was never evaluated and # the URI itself didn't resolve to a working endpoint — commonly a wrong GHE base URI. if ($null -eq $code) { Write-Host "`nGitHub API base URI '$ApiBaseUri' is not reachable (no response). Verify network connectivity, DNS/TLS, and the GitHubApiBaseUri value (use https://api.{subdomain}.ghe.com for GHE.com)." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'ApiBaseUriFailed' } return } # 5xx means GitHub responded but the endpoint is failing — the URI is fine, the # service is unavailable. Don't conflate with token or base-URI problems. if ($code -ge 500 -and $code -le 599) { Write-Host "`nGitHub API request failed (HTTP $code). The GitHub service may be temporarily unavailable; check https://www.githubstatus.com/ and retry." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'ApiUnavailable' } return } if ($code -eq 401) { Write-Host "`nGitHub token validation failed (HTTP 401). Verify the GitHub authorization is valid and not expired." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'TokenInvalid' } return } Write-Host "`nGitHub token validation failed (HTTP $code). Verify the GitHub authorization is valid and not expired." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'TokenInvalid' } return } # Probe 2: org access $encodedOrg = [System.Uri]::EscapeDataString($Organization) $orgInstallPrompted = $false $orgData = $null $planName = $null $role = $null $roleState = $null $roleWarning = $null while ($true) { $orgData = $null $planName = $null $role = $null $roleState = $null $roleWarning = $null try { $orgResponse = Invoke-WebRequest -Uri "$ApiBaseUri/orgs/$encodedOrg" -Headers $authHeaders -Method GET -UseBasicParsing -ErrorAction Stop $orgData = $orgResponse.Content | ConvertFrom-Json } catch { $rateLimitMessage = Get-MtGitHubRateLimitMessage -ErrorRecord $_ if ($rateLimitMessage) { Write-Host "`n$rateLimitMessage" -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'RateLimited' } return } $code = Get-MtGitHubErrorStatusCode -ErrorRecord $_ $apiMsg = Get-MtGitHubErrorMessage -ErrorRecord $_ # Same transport/5xx classification as /user — a probe that gets to the network can # still fail for reasons unrelated to org access (DNS hiccup, partial outage), and # those should report ApiBaseUriFailed / ApiUnavailable rather than OrgAccessFailed. if ($null -eq $code) { Write-Host "`nGitHub API base URI '$ApiBaseUri' is not reachable (no response) while probing /orgs/$Organization. Verify network connectivity, DNS/TLS, and the GitHubApiBaseUri value." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'ApiBaseUriFailed' } return } if ($code -ge 500 -and $code -le 599) { Write-Host "`nGitHub API request failed (HTTP $code) while probing /orgs/$Organization. The GitHub service may be temporarily unavailable; check https://www.githubstatus.com/ and retry." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'ApiUnavailable' } return } if ($authenticationType -eq 'GitHubAppDeviceFlow' -and -not $orgInstallPrompted -and $code -eq 403) { $orgInstallPrompted = Request-MtGitHubAppOrganizationInstall -Organization $Organization -InstallUrl $maesterGitHubAppInstallUrl -Reason $apiMsg if ($orgInstallPrompted) { continue } } $msg = switch ($code) { 403 { "Access denied (403). For Maester GitHub App auth, install/approve the app for '$Organization' ($maesterGitHubAppInstallUrl) and authorize as an organization member or owner. For token auth, verify the token has the required organization permissions. GitHub API: $apiMsg" } 404 { "Organization '$Organization' not found (404). Check the organization login name. GitHub API: $apiMsg" } default { "HTTP $code. $apiMsg" } } Write-Host "`nFailed to access GitHub organization: $msg" -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'OrgAccessFailed' } return } # Null-safe plan name (not visible for all token types/roles) if ($orgData.PSObject.Properties.Name -contains 'plan' -and $null -ne $orgData.plan) { $planName = $orgData.plan.name } # Probe 3: signed-in user's org membership / role — blocking proof of org access. # /orgs/{org} returns public org metadata even for tokens with no relationship to the org. # Use /user/memberships/orgs/{org} so the token only has to prove its own membership, # not read an arbitrary /orgs/{org}/memberships/{username} record. try { $membershipResponse = Invoke-WebRequest -Uri "$ApiBaseUri/user/memberships/orgs/$encodedOrg" -Headers $authHeaders -Method GET -UseBasicParsing -ErrorAction Stop $membershipData = $null try { $membershipData = $membershipResponse.Content | ConvertFrom-Json -ErrorAction Stop } catch { $membershipData = $null } $hasState = $null -ne $membershipData -and $membershipData.PSObject.Properties.Name -contains 'state' -and -not [string]::IsNullOrWhiteSpace([string]$membershipData.state) $hasRole = $null -ne $membershipData -and $membershipData.PSObject.Properties.Name -contains 'role' -and -not [string]::IsNullOrWhiteSpace([string]$membershipData.role) if (-not ($hasState -and $hasRole)) { Write-Host "`nGitHub organization membership could not be verified: malformed response from /user/memberships/orgs/$Organization." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'OrgMembershipFailed' } return } $role = [string]$membershipData.role $roleState = [string]$membershipData.state # Only an 'active' membership proves the user can actually act on the org. # 'pending' (invitation not accepted) and any other non-active state should # not be treated as a usable session — org-scoped controls would silently # fail or report misleading data. Fail closed and do not retain auth state. if ($roleState -ne 'active') { Write-Host "`nGitHub organization membership is not active (state: '$roleState'). Accept the organization invitation in GitHub and reconnect." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'OrgMembershipPending' } return } if ($role -ne 'admin') { $roleWarning = "GitHub organization admin/owner permissions required for full CIS coverage. Current role: '$role'. Some controls may skip or report limited visibility." } } catch { $rateLimitMessage = Get-MtGitHubRateLimitMessage -ErrorRecord $_ if ($rateLimitMessage) { Write-Host "`n$rateLimitMessage" -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'RateLimited' } return } $code = Get-MtGitHubErrorStatusCode -ErrorRecord $_ $apiMsg = Get-MtGitHubErrorMessage -ErrorRecord $_ # Same transport/5xx classification as /user — separating "the network/service broke" # from "the token cannot prove membership" matters for actionable diagnostics. if ($null -eq $code) { Write-Host "`nGitHub API base URI '$ApiBaseUri' is not reachable (no response) while probing membership for '$($user.login)' in '$Organization'." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'ApiBaseUriFailed' } return } if ($code -ge 500 -and $code -le 599) { Write-Host "`nGitHub API request failed (HTTP $code) while probing membership for '$($user.login)' in '$Organization'. Service may be temporarily unavailable; check https://www.githubstatus.com/ and retry." -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'ApiUnavailable' } return } if ($authenticationType -eq 'GitHubAppDeviceFlow' -and -not $orgInstallPrompted -and $code -eq 403) { $orgInstallPrompted = Request-MtGitHubAppOrganizationInstall -Organization $Organization -InstallUrl $maesterGitHubAppInstallUrl -Reason $apiMsg if ($orgInstallPrompted) { continue } } $msg = switch ($code) { 403 { "Membership probe forbidden (403). The token cannot prove membership in '$Organization'. For Maester GitHub App auth, confirm the app is installed/approved for '$Organization' ($maesterGitHubAppInstallUrl) and that '$($user.login)' is an active organization member. GitHub API: $apiMsg" } 404 { "The signed-in user '$($user.login)' is not an active member of organization '$Organization' (404). GitHub API: $apiMsg" } default { "Membership probe failed (HTTP $code). $apiMsg" } } Write-Host "`nFailed to verify GitHub organization membership: $msg" -ForegroundColor Red $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'OrgMembershipFailed' } return } break } # Probe 4: organization administration access (non-blocking). # /orgs/{org}/actions/permissions requires classic PAT 'admin:org' or fine-grained # 'Organization Administration: read' — a closer match to the permissions needed by # CIS org-admin controls than the membership endpoint. Failure here records the # outcome and emits a warning, but does not flip Connected to $false. $adminVerified = $false $adminFailureReason = $null $adminStatusCode = $null $adminAcceptedPermissions = $null $adminWarning = $null try { $adminResponse = Invoke-WebRequest -Uri "$ApiBaseUri/orgs/$encodedOrg/actions/permissions" -Headers $authHeaders -Method GET -UseBasicParsing -ErrorAction Stop $adminVerified = $true $adminStatusCode = 200 if ($adminResponse.PSObject.Properties.Name -contains 'StatusCode' -and $null -ne $adminResponse.StatusCode) { $adminStatusCode = [int]$adminResponse.StatusCode } } catch { $adminStatusCode = Get-MtGitHubErrorStatusCode -ErrorRecord $_ $adminApiMsg = Get-MtGitHubErrorMessage -ErrorRecord $_ $respHeaders = $null if ($null -ne $_.Exception -and $null -ne $_.Exception.Response) { $respHeaders = $_.Exception.Response.Headers } $adminAcceptedPermissions = Get-MtGitHubResponseHeaderValue -Headers $respHeaders -Name 'x-accepted-github-permissions' $adminRateLimitMessage = Get-MtGitHubRateLimitMessage -ErrorRecord $_ if ($adminRateLimitMessage) { $adminFailureReason = $adminRateLimitMessage $adminWarning = "GitHub organization administration API access was not verified due to a GitHub API rate limit. Detail: $adminRateLimitMessage" } else { $adminFailureReason = switch ($adminStatusCode) { 403 { "HTTP 403 from /orgs/$Organization/actions/permissions. $adminApiMsg" } 404 { "HTTP 404 from /orgs/$Organization/actions/permissions. $adminApiMsg" } default { "HTTP $adminStatusCode from /orgs/$Organization/actions/permissions. $adminApiMsg" } } $adminWarning = "GitHub organization administration API access was not verified. Some CIS controls requiring org administration may skip or report limited visibility. Required permissions — Maester GitHub App: Organization Administration: read; classic PAT: admin:org; fine-grained PAT: Organization Administration: read. Detail: $adminFailureReason" } } $__MtSession.GitHubAuthHeader = $authHeaders $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $true Organization = $Organization ApiBaseUri = $ApiBaseUri ApiVersion = $ApiVersion TokenLogin = $user.login Plan = $planName Role = $role RoleState = $roleState RoleVerified = $true RoleVerificationFailureReason = $null AuthenticationType = $authenticationType TokenExpiresAt = $tokenExpiresAt AdministrationPermissionVerified = $adminVerified AdministrationPermissionFailureReason = $adminFailureReason AdministrationPermissionStatusCode = $adminStatusCode AdministrationPermissionAcceptedPermissions = $adminAcceptedPermissions FailureReason = $null } if ($roleWarning) { Write-Warning $roleWarning } if ($adminWarning) { Write-Warning $adminWarning } $planDisplay = if ($planName) { " (plan: $planName)" } else { '' } Write-Host "Connected to GitHub organization '$($orgData.login)' as '$($user.login)'$planDisplay." -ForegroundColor Green } |