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..cbf02acf2 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',
@@ -128,7 +128,10 @@
'Test-MtCisConnectionFilterSafeList', 'Test-MtCisCreateTenantDisallowed', 'Test-MtCisCustomerLockBox',
'Test-MtCisDevicesWithoutCompliancePolicyMarked', 'Test-MtCisDkim', 'Test-MtCisEnsureGuestAccessRestricted',
'Test-MtCisEnsureGuestUserDynamicGroup', 'Test-MtCisEnsureUserConsentToAppsDisallowed', 'Test-MtCisExoAdditionalStorageProvider',
- 'Test-MtCisFormsPhishingProtectionEnabled', 'Test-MtCisGlobalAdminCount', 'Test-MtCisHostedConnectionFilterPolicy',
+ 'Test-MtCisFormsPhishingProtectionEnabled', 'Test-MtCisGitHubIssueDeletionLimited',
+ 'Test-MtCisGitHubRepositoryCreationLimited', 'Test-MtCisGitHubRepositoryDeletionLimited',
+ 'Test-MtCisGitHubStrictBasePermission', 'Test-MtCisGitHubTeamCreationLimited',
+ 'Test-MtCisGlobalAdminCount', 'Test-MtCisHostedConnectionFilterPolicy',
'Test-MtCisInternalMalwareNotification', 'Test-MtCisOutboundSpamFilterPolicy', 'Test-MtCisPasswordExpiry',
'Test-MtCisSafeAntiPhishingPolicy', 'Test-MtCisSafeAttachment', 'Test-MtCisSafeAttachmentsAtpPolicy',
'Test-MtCisSafeLink', 'Test-MtCisSharedMailboxSignIn', 'Test-MtCisTeamsLobbyBypass',
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-MtGitHubCacheKey.ps1 b/powershell/internal/Get-MtGitHubCacheKey.ps1
new file mode 100644
index 000000000..a34a2c53b
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubCacheKey.ps1
@@ -0,0 +1,17 @@
+function Get-MtGitHubCacheKey {
+ <#
+ .SYNOPSIS
+ Internal: Builds the per-session GitHub REST cache key.
+ #>
+ [CmdletBinding()]
+ [OutputType([string])]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string] $ApiVersion,
+
+ [Parameter(Mandatory = $true)]
+ [string] $AbsoluteUri
+ )
+
+ return "$ApiVersion|$AbsoluteUri"
+}
diff --git a/powershell/internal/Get-MtGitHubErrorMessage.ps1 b/powershell/internal/Get-MtGitHubErrorMessage.ps1
new file mode 100644
index 000000000..43b4151b3
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubErrorMessage.ps1
@@ -0,0 +1,27 @@
+function Get-MtGitHubErrorMessage {
+ <#
+ .SYNOPSIS
+ Internal: Extracts the most useful GitHub API error message from an ErrorRecord.
+
+ .DESCRIPTION
+ Prefers a JSON ErrorDetails.Message `message` property when present, falls back to
+ the raw error-details string, then to the exception message or full ErrorRecord text.
+ #>
+ 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..d1769dfc2
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubErrorStatusCode.ps1
@@ -0,0 +1,19 @@
+function Get-MtGitHubErrorStatusCode {
+ <#
+ .SYNOPSIS
+ Internal: Extracts an HTTP status code from a GitHub API ErrorRecord.
+
+ .DESCRIPTION
+ Returns the integer status code when the ErrorRecord includes an HTTP response, or
+ $null for transport failures where no HTTP response was produced.
+ #>
+ 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-MtGitHubOrganization.ps1 b/powershell/internal/Get-MtGitHubOrganization.ps1
new file mode 100644
index 000000000..fcbaa2e0d
--- /dev/null
+++ b/powershell/internal/Get-MtGitHubOrganization.ps1
@@ -0,0 +1,17 @@
+function Get-MtGitHubOrganization {
+ <#
+ .SYNOPSIS
+ Internal: Gets the connected GitHub organization object using the session cache.
+ #>
+ [CmdletBinding()]
+ param()
+
+ if ($null -eq $__MtSession.GitHubConnection -or
+ $__MtSession.GitHubConnection.Connected -ne $true) {
+ throw "Not connected to GitHub. Call Connect-MtGitHub first."
+ }
+
+ # Connect-MtGitHub stores the raw organization login; encode it here for the path segment.
+ $encodedOrg = [System.Uri]::EscapeDataString($__MtSession.GitHubConnection.Organization)
+ Invoke-MtGitHubRequest -RelativeUri "/orgs/$encodedOrg"
+}
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: