Skip to content

Commit 802701e

Browse files
Merge pull request KelvinTegelaar#1741 from MWG-Logan/ga-list-alert
Add Get-CIPPAlertGlobalAdminAllowList function and tests
2 parents 2f6ac75 + edfe33a commit 802701e

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
function Get-CIPPAlertGlobalAdminAllowList {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
param (
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$InputValue,
11+
$TenantFilter
12+
)
13+
try {
14+
$AllowedAdmins = @()
15+
$AlertEachAdmin = $false
16+
if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) {
17+
$AlertEachAdmin = [bool]($InputValue['AlertEachAdmin'])
18+
$ApprovedValue = if ($InputValue.ContainsKey('ApprovedGlobalAdmins') -or ($InputValue.PSObject.Properties.Name -contains 'ApprovedGlobalAdmins')) {
19+
$InputValue['ApprovedGlobalAdmins']
20+
} else {
21+
$null
22+
}
23+
$InputValue = $ApprovedValue
24+
}
25+
if ($null -ne $InputValue) {
26+
if ($InputValue -is [string]) {
27+
$AllowedAdmins = $InputValue -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
28+
} elseif ($InputValue -is [System.Collections.IEnumerable]) {
29+
$AllowedAdmins = $InputValue | ForEach-Object { $_.ToString().Trim() } | Where-Object { $_ }
30+
} else {
31+
$AllowedAdmins = @("$InputValue")
32+
}
33+
}
34+
$AllowedLookup = $AllowedAdmins | ForEach-Object { $_.ToLowerInvariant() } | Select-Object -Unique
35+
36+
if (-not $AllowedLookup -or $AllowedLookup.Count -eq 0) {
37+
return
38+
}
39+
40+
$GlobalAdmins = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$select=id,displayName,userPrincipalName" -tenantid $TenantFilter -AsApp $true -ErrorAction Stop | Where-Object {
41+
$_.'@odata.type' -eq '#microsoft.graph.user' -and $_.displayName -ne 'On-Premises Directory Synchronization Service Account'
42+
}
43+
44+
$UnapprovedAdmins = foreach ($admin in $GlobalAdmins) {
45+
if ([string]::IsNullOrWhiteSpace($admin.userPrincipalName)) { continue }
46+
$UpnPrefix = ($admin.userPrincipalName -split '@')[0].ToLowerInvariant()
47+
if ($AllowedLookup -notcontains $UpnPrefix) {
48+
[PSCustomObject]@{
49+
Admin = $admin
50+
UpnPrefix = $UpnPrefix
51+
}
52+
}
53+
}
54+
55+
if ($UnapprovedAdmins) {
56+
if ($AlertEachAdmin) {
57+
$AlertData = foreach ($item in $UnapprovedAdmins) {
58+
$admin = $item.Admin
59+
$UpnPrefix = $item.UpnPrefix
60+
[PSCustomObject]@{
61+
Message = "$($admin.userPrincipalName) has Global Administrator role but is not in the approved allow list (prefix '$UpnPrefix')."
62+
DisplayName = $admin.displayName
63+
UserPrincipalName = $admin.userPrincipalName
64+
Id = $admin.id
65+
AllowedList = if ($AllowedAdmins) { $AllowedAdmins -join ', ' } else { 'Not provided' }
66+
Tenant = $TenantFilter
67+
}
68+
}
69+
} else {
70+
$NonCompliantUpns = @($UnapprovedAdmins.Admin.userPrincipalName)
71+
$AlertData = @([PSCustomObject]@{
72+
Message = "Found $($NonCompliantUpns.Count) Global Administrator account(s) not in the approved allow list."
73+
NonCompliantUsers = $NonCompliantUpns
74+
ApprovedPrefixes = if ($AllowedAdmins) { $AllowedAdmins -join ', ' } else { 'Not provided' }
75+
Tenant = $TenantFilter
76+
})
77+
}
78+
79+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
80+
}
81+
} catch {
82+
Write-AlertMessage -tenant $TenantFilter -message "Failed to check approved Global Admins: $(Get-NormalizedError -message $_.Exception.Message)"
83+
}
84+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Pester tests for Get-CIPPAlertGlobalAdminAllowList
2+
# Verifies prefix-based allow list handling and alert emission
3+
4+
BeforeAll {
5+
$RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
6+
$AlertPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminAllowList.ps1'
7+
8+
# Provide minimal stubs so Mock has commands to replace during tests
9+
function New-GraphGetRequest { param($uri, $tenantid, $AsApp) }
10+
function Write-AlertTrace { param($cmdletName, $tenantFilter, $data) }
11+
function Write-AlertMessage { param($tenant, $message) }
12+
function Get-NormalizedError { param($message) $message }
13+
14+
. $AlertPath
15+
}
16+
17+
Describe 'Get-CIPPAlertGlobalAdminAllowList' {
18+
BeforeEach {
19+
$script:CapturedData = $null
20+
$script:CapturedTenant = $null
21+
$script:CapturedErrorMessage = $null
22+
23+
Mock -CommandName New-GraphGetRequest -MockWith {
24+
@(
25+
[pscustomobject]@{
26+
'@odata.type' = '#microsoft.graph.user'
27+
displayName = 'Allowed Admin'
28+
userPrincipalName = '[email protected]'
29+
id = 'id-allowed'
30+
},
31+
[pscustomobject]@{
32+
'@odata.type' = '#microsoft.graph.user'
33+
displayName = 'Unapproved Admin'
34+
userPrincipalName = '[email protected]'
35+
id = 'id-unapproved'
36+
}
37+
)
38+
}
39+
40+
Mock -CommandName Write-AlertTrace -MockWith {
41+
param($cmdletName, $tenantFilter, $data)
42+
$script:CapturedData = $data
43+
$script:CapturedTenant = $tenantFilter
44+
}
45+
46+
Mock -CommandName Write-AlertMessage -MockWith {
47+
param($tenant, $message)
48+
$script:CapturedErrorMessage = $message
49+
}
50+
}
51+
52+
It 'emits per-admin alerts when AlertEachAdmin is true' {
53+
$allowInput = @{ ApprovedGlobalAdmins = 'breakglass'; AlertEachAdmin = $true }
54+
55+
Get-CIPPAlertGlobalAdminAllowList -TenantFilter 'contoso.onmicrosoft.com' -InputValue $allowInput
56+
57+
$CapturedData | Should -Not -BeNullOrEmpty
58+
$CapturedData.UserPrincipalName | Should -Contain '[email protected]'
59+
$CapturedData.UserPrincipalName | Should -Not -Contain '[email protected]'
60+
$CapturedTenant | Should -Be 'contoso.onmicrosoft.com'
61+
}
62+
63+
It 'emits single aggregated alert when AlertEachAdmin is false (default)' {
64+
Get-CIPPAlertGlobalAdminAllowList -TenantFilter 'contoso.onmicrosoft.com' -InputValue 'breakglass'
65+
66+
$CapturedData | Should -Not -BeNullOrEmpty
67+
$CapturedData.Count | Should -Be 1
68+
$CapturedData[0].NonCompliantUsers | Should -Contain '[email protected]'
69+
$CapturedData[0].NonCompliantUsers | Should -Not -Contain '[email protected]'
70+
}
71+
72+
It 'emits single aggregated alert when AlertEachAdmin is explicitly false via input object' {
73+
$allowInput = @{ ApprovedGlobalAdmins = 'breakglass'; AlertEachAdmin = $false }
74+
75+
Get-CIPPAlertGlobalAdminAllowList -TenantFilter 'contoso.onmicrosoft.com' -InputValue $allowInput
76+
77+
$CapturedData | Should -Not -BeNullOrEmpty
78+
$CapturedData.Count | Should -Be 1
79+
$CapturedData[0].NonCompliantUsers | Should -Contain '[email protected]'
80+
$CapturedData[0].NonCompliantUsers | Should -Not -Contain '[email protected]'
81+
}
82+
83+
It 'suppresses alert when UPN prefix is approved (comma separated list)' {
84+
$allowInput = @{ ApprovedGlobalAdmins = 'breakglass,otheradmin'; AlertEachAdmin = $true }
85+
Get-CIPPAlertGlobalAdminAllowList -TenantFilter 'contoso.onmicrosoft.com' -InputValue $allowInput
86+
87+
$CapturedData | Should -BeNullOrEmpty
88+
}
89+
90+
It 'accepts ApprovedGlobalAdmins property when provided as hashtable' {
91+
$allowInput = @{ ApprovedGlobalAdmins = 'breakglass,otheradmin' }
92+
Get-CIPPAlertGlobalAdminAllowList -TenantFilter 'contoso.onmicrosoft.com' -InputValue $allowInput
93+
94+
$CapturedData | Should -BeNullOrEmpty
95+
}
96+
97+
It 'writes alert message when Graph call fails' {
98+
Mock -CommandName New-GraphGetRequest -MockWith { throw 'Graph failure' } -Verifiable
99+
100+
Get-CIPPAlertGlobalAdminAllowList -TenantFilter 'contoso.onmicrosoft.com' -InputValue 'breakglass'
101+
102+
$CapturedData | Should -BeNullOrEmpty
103+
$CapturedErrorMessage | Should -Match 'Failed to check approved Global Admins'
104+
$CapturedErrorMessage | Should -Match 'Graph failure'
105+
}
106+
}

0 commit comments

Comments
 (0)