diff --git a/powershell/Maester.Format.ps1xml b/powershell/Maester.Format.ps1xml
index 57eb7dd89..dfeeddb68 100644
--- a/powershell/Maester.Format.ps1xml
+++ b/powershell/Maester.Format.ps1xml
@@ -103,6 +103,28 @@
}
+
+
+
+ $null -ne $_.GitHub
+
+
+
+
+ if ($_.GitHub) {
+ $connectedText = if ($_.GitHub.Connected) { 'Connected' } else { 'Not connected' }
+ "$connectedText`n" +
+ "Organization: $($_.GitHub.Organization)`n" +
+ "Token Login: $($_.GitHub.TokenLogin)`n" +
+ "API Base URI: $($_.GitHub.ApiBaseUri)`n" +
+ "Role: $($_.GitHub.Role)`n" +
+ "Role State: $($_.GitHub.RoleState)`n" +
+ "Administration Permission Verified: $($_.GitHub.AdministrationPermissionVerified)`n"
+ } else {
+ ''
+ }
+
+
diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1
index a8142ff5c..2a9110241 100644
--- a/powershell/Maester.psd1
+++ b/powershell/Maester.psd1
@@ -57,9 +57,9 @@
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @(
'Add-MtMaesterAppFederatedCredential', 'Add-MtTestResultDetail', 'Clear-MtDnsCache', 'Clear-MtExoCache',
- 'Clear-MtGraphCache', 'Compare-MtJsonObject', 'Compare-MtTestResult', 'Connect-Maester', 'Convert-MtResultsToFlatObject',
+ 'Clear-MtGraphCache', 'Compare-MtJsonObject', 'Compare-MtTestResult', 'Connect-Maester', 'Connect-MtGitHub', 'Convert-MtResultsToFlatObject',
'ConvertFrom-MailAuthenticationRecordDkim', 'ConvertFrom-MailAuthenticationRecordDmarc',
- 'ConvertFrom-MailAuthenticationRecordMx', 'ConvertFrom-MailAuthenticationRecordSpf', 'Disconnect-Maester',
+ 'ConvertFrom-MailAuthenticationRecordMx', 'ConvertFrom-MailAuthenticationRecordSpf', 'Disconnect-Maester', 'Disconnect-MtGitHub',
'Get-MailAuthenticationRecord', 'Get-MtAdminPortalUrl', 'Get-MtAuthenticationMethodPolicyConfig',
'Get-MtAzureManagementGroup', 'Get-MtConditionalAccessPolicy', 'Get-MtExo', 'Get-MtExoThreatPolicyMalware',
'Get-MtGraphScope', 'Get-MtGroupMember', 'Get-MtHtmlReport', 'Get-MtLicenseInformation', 'Get-MtMaesterApp', 'Get-MtRole',
diff --git a/powershell/Maester.psm1 b/powershell/Maester.psm1
index 1cb989e9f..3dd22854f 100644
--- a/powershell/Maester.psm1
+++ b/powershell/Maester.psm1
@@ -25,6 +25,7 @@ $__MtSession = @{
DataverseApiBase = $null # Resolved Dataverse OData API base URL (e.g. https://org123.api.crm.dynamics.com/api/data/v9.2)
DataverseResourceUrl = $null # Dataverse resource URL for token acquisition (e.g. https://org123.crm.dynamics.com)
DataverseEnvironmentId = $null # Environment identifier for display (e.g. org123.crm.dynamics.com)
+ GitHubCache = @{} # Per-session REST response cache; cleared each Invoke-Maester run
}
New-Variable -Name __MtSession -Value $__MtSession -Scope Script -Force
diff --git a/powershell/internal/Clear-ModuleVariable.ps1 b/powershell/internal/Clear-ModuleVariable.ps1
index 17879e660..bb456aabd 100644
--- a/powershell/internal/Clear-ModuleVariable.ps1
+++ b/powershell/internal/Clear-ModuleVariable.ps1
@@ -21,5 +21,8 @@
Clear-MtExoCache
$__MtSession.AIAgentInfo = $null
$__MtSession.AzureDevOpsConnection = $null
+ # Do not clear GitHubConnection or GitHubAuthHeader — the user calls Connect-MtGitHub before
+ # Invoke-Maester; the session must persist across runs. Only the per-run cache is reset.
+ $__MtSession.GitHubCache = @{}
# $__MtSession.Connections = @() # Do not clear connections as they are used to track the connection state. This module variable should only be set by Connect-Maester and Disconnect-Maester.
}
diff --git a/powershell/internal/Get-MtGitHubErrorMessage.ps1 b/powershell/internal/Get-MtGitHubErrorMessage.ps1
new file mode 100644
index 000000000..6ce0b5659
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubErrorMessage.ps1
@@ -0,0 +1,19 @@
+function Get-MtGitHubErrorMessage {
+ param([Parameter(Mandatory)] $ErrorRecord)
+ if (-not [string]::IsNullOrEmpty($ErrorRecord.ErrorDetails.Message)) {
+ try {
+ $parsed = $ErrorRecord.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop
+ if ($parsed.PSObject.Properties.Name -contains 'message' -and
+ -not [string]::IsNullOrEmpty($parsed.message)) {
+ return $parsed.message
+ }
+ } catch {
+ Write-Debug "Get-MtGitHubErrorMessage: ErrorDetails.Message is not JSON, returning raw string."
+ }
+ return $ErrorRecord.ErrorDetails.Message
+ }
+ if (-not [string]::IsNullOrEmpty($ErrorRecord.Exception.Message)) {
+ return $ErrorRecord.Exception.Message
+ }
+ return ($ErrorRecord | Out-String)
+}
diff --git a/powershell/internal/Get-MtGitHubErrorStatusCode.ps1 b/powershell/internal/Get-MtGitHubErrorStatusCode.ps1
new file mode 100644
index 000000000..046d6c590
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubErrorStatusCode.ps1
@@ -0,0 +1,11 @@
+function Get-MtGitHubErrorStatusCode {
+ param([Parameter(Mandatory)] $ErrorRecord)
+ try {
+ if ($ErrorRecord.Exception.Response -and $ErrorRecord.Exception.Response.StatusCode) {
+ return [int]$ErrorRecord.Exception.Response.StatusCode
+ }
+ } catch {
+ Write-Debug "Get-MtGitHubErrorStatusCode: $($_.Exception.Message)"
+ }
+ return $null
+}
diff --git a/powershell/internal/Get-MtGitHubRateLimitMessage.ps1 b/powershell/internal/Get-MtGitHubRateLimitMessage.ps1
new file mode 100644
index 000000000..f55609689
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubRateLimitMessage.ps1
@@ -0,0 +1,75 @@
+function Get-MtGitHubRateLimitMessage {
+ <#
+ .SYNOPSIS
+ Internal: Returns a GitHub rate-limit message for an ErrorRecord, or $null when the
+ error is not a rate-limit response.
+
+ .DESCRIPTION
+ Mirrors the rate-limit detection in Invoke-MtGitHubRequest so that bootstrap callers
+ (Connect-MtGitHub) can distinguish HTTP 403/429 caused by rate limiting from
+ permission, token, or org-access failures.
+
+ Returns:
+ - "GitHub API rate limit encountered (HTTP ). Resets at: