Skip to content

Commit 08fcd89

Browse files
authored
Merge pull request #9 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 09c5b3b + 2e9a2b4 commit 08fcd89

101 files changed

Lines changed: 4289 additions & 609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraLicenseUtilization.ps1

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function Get-CIPPAlertEntraLicenseUtilization {
44
Entrypoint
55
#>
66
[CmdletBinding()]
7-
Param (
7+
param (
88
[Parameter(Mandatory = $false)]
99
[Alias('input')]
1010
$InputValue,
@@ -15,34 +15,52 @@ function Get-CIPPAlertEntraLicenseUtilization {
1515
$Threshold = if ($InputValue) { [int]$InputValue } else { 110 }
1616

1717
$LicenseData = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/reports/azureADPremiumLicenseInsight' -tenantid $($TenantFilter)
18-
$Alerts = [System.Collections.Generic.List[string]]::new()
1918

20-
# Check P1 License utilization
21-
if ($LicenseData.entitledP1LicenseCount -gt 0 -or $LicenseData.entitledP2LicenseCount -gt 0) {
22-
$P1Used = $LicenseData.p1FeatureUtilizations.conditionalAccess.userCount
23-
$P1Entitled = $LicenseData.entitledP1LicenseCount + $LicenseData.entitledP2LicenseCount
24-
$P1Usage = ($P1Used / $P1Entitled) * 100
25-
$P1Overage = $P1Used - $P1Entitled
19+
$AlertData = @(
20+
# Check P1 License utilization
21+
if ($LicenseData.entitledP1LicenseCount -gt 0 -or $LicenseData.entitledP2LicenseCount -gt 0) {
22+
$P1Used = $LicenseData.p1FeatureUtilizations.conditionalAccess.userCount
23+
$P1Entitled = $LicenseData.entitledP1LicenseCount + $LicenseData.entitledP2LicenseCount
24+
$P1Usage = [math]::Round(($P1Used / $P1Entitled) * 100, 2)
25+
$P1Overage = $P1Used - $P1Entitled
2626

27-
if ($P1Usage -gt $Threshold -and $P1Overage -ge 5) {
28-
$Alerts.Add("P1 License utilization is at $([math]::Round($P1Usage,2))% (Using $P1Used of $P1Entitled licenses, over by $P1Overage)")
27+
if ($P1Usage -gt $Threshold -and $P1Overage -ge 5) {
28+
[PSCustomObject]@{
29+
Message = "Entra ID P1 license utilization is at $P1Usage% (using $P1Used of $P1Entitled licenses, over by $P1Overage)"
30+
LicenseType = 'Entra ID P1'
31+
UsedLicenses = $P1Used
32+
EntitledLicenses = $P1Entitled
33+
UsagePercent = $P1Usage
34+
Overage = $P1Overage
35+
Threshold = $Threshold
36+
Tenant = $TenantFilter
37+
}
38+
}
2939
}
30-
}
3140

32-
# Check P2 License utilization
33-
if ($LicenseData.entitledP2LicenseCount -gt 0) {
34-
$P2Used = $LicenseData.p2FeatureUtilizations.riskBasedConditionalAccess.userCount
35-
$P2Entitled = $LicenseData.entitledP2LicenseCount
36-
$P2Usage = ($P2Used / $P2Entitled) * 100
37-
$P2Overage = $P2Used - $P2Entitled
41+
# Check P2 License utilization
42+
if ($LicenseData.entitledP2LicenseCount -gt 0) {
43+
$P2Used = $LicenseData.p2FeatureUtilizations.riskBasedConditionalAccess.userCount
44+
$P2Entitled = $LicenseData.entitledP2LicenseCount
45+
$P2Usage = [math]::Round(($P2Used / $P2Entitled) * 100, 2)
46+
$P2Overage = $P2Used - $P2Entitled
3847

39-
if ($P2Usage -gt $Threshold -and $P2Overage -ge 5) {
40-
$Alerts.Add("P2 License utilization is at $([math]::Round($P2Usage,2))% (Using $P2Used of $P2Entitled licenses, over by $P2Overage)")
48+
if ($P2Usage -gt $Threshold -and $P2Overage -ge 5) {
49+
[PSCustomObject]@{
50+
Message = "Entra ID P2 license utilization is at $P2Usage% (using $P2Used of $P2Entitled licenses, over by $P2Overage)"
51+
LicenseType = 'Entra ID P2'
52+
UsedLicenses = $P2Used
53+
EntitledLicenses = $P2Entitled
54+
UsagePercent = $P2Usage
55+
Overage = $P2Overage
56+
Threshold = $Threshold
57+
Tenant = $TenantFilter
58+
}
59+
}
4160
}
42-
}
61+
)
4362

44-
if ($Alerts.Count -gt 0) {
45-
$AlertData = "License Over-utilization Alert (Threshold: $Threshold%, Min Overage: 5): $($Alerts -join ' | ')"
63+
if ($AlertData.Count -gt 0) {
4664
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
4765
}
4866

Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAlertUsers.ps1

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ function Get-CIPPAlertMFAAlertUsers {
1111
$TenantFilter
1212
)
1313
try {
14-
$MFAReport = try { Get-CIPPMFAStateReport -TenantFilter $TenantFilter | Where-Object { $_.DisplayName -ne 'On-Premises Directory Synchronization Service Account' } } catch { $null }
14+
Write-Host "Checking MFA status for users in tenant '$TenantFilter'..."
15+
$MFAReport = try { Get-CIPPMFAStateReport -TenantFilter $TenantFilter | Where-Object { $_.DisplayName -ne 'On-Premises Directory Synchronization Service Account' } } catch { Write-Host "Could not get cached report for tenant '$TenantFilter' $_" }
1516

1617
$Users = if ($MFAReport) {
1718
$MFAReport | Where-Object { $_.IsAdmin -ne $true -and $_.MFARegistration -eq $false -and $_.UserType -ne 'Guest' -and $_.UPN -notmatch '^package_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}@' }
1819
} else {
1920
New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq false and isMfaRegistered eq false and userType eq 'member'&`$select=userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true |
20-
Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' -and $_.userPrincipalName -notmatch '^package_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}@' } |
21-
Select-Object @{n = 'UPN'; e = { $_.userPrincipalName } }, @{n = 'DisplayName'; e = { $_.userDisplayName } }
21+
Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' -and $_.userPrincipalName -notmatch '^package_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}@' } |
22+
Select-Object @{n = 'UPN'; e = { $_.userPrincipalName } }, @{n = 'DisplayName'; e = { $_.userDisplayName } }
2223
}
24+
Write-Host "Completed MFA status check for tenant '$TenantFilter'. Found $($Users.Count) users without MFA registered."
2325

2426
if ($Users) {
2527
$AlertData = foreach ($user in $Users) {
@@ -28,6 +30,7 @@ function Get-CIPPAlertMFAAlertUsers {
2830
DisplayName = $user.DisplayName
2931
}
3032
}
33+
Write-Host 'Writing alert trace'
3134
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
3235
}
3336

Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ function Get-CIPPAlertMXRecordChanged {
4141
}
4242

4343
if ($Differences) {
44-
"$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]"
44+
[PSCustomObject]@{
45+
Message = "$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]"
46+
Domain = $Domain.Domain
47+
PreviousRecords = $PreviousRecords -join ', '
48+
CurrentRecords = $CurrentRecords -join ', '
49+
Tenant = $TenantFilter
50+
}
4551
}
4652
} catch {
4753
Write-Information "Error checking domain $($Domain.Domain): $($_.Exception.Message)"
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
function Get-CIPPAlertTenantAccess {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
[CmdletBinding()]
7+
param(
8+
[Parameter(Mandatory = $false)]
9+
[Alias('input')]
10+
$InputValue,
11+
$TenantFilter
12+
)
13+
14+
$ExpectedRoles = @(
15+
@{ Name = 'Application Administrator'; Id = '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3' },
16+
@{ Name = 'User Administrator'; Id = 'fe930be7-5e62-47db-91af-98c3a49a38b1' },
17+
@{ Name = 'Intune Administrator'; Id = '3a2c62db-5318-420d-8d74-23affee5d9d5' },
18+
@{ Name = 'Exchange Administrator'; Id = '29232cdf-9323-42fd-ade2-1d097af3e4de' },
19+
@{ Name = 'Security Administrator'; Id = '194ae4cb-b126-40b2-bd5b-6091b380977d' },
20+
@{ Name = 'Cloud App Security Administrator'; Id = '892c5842-a9a6-463a-8041-72aa08ca3cf6' },
21+
@{ Name = 'Cloud Device Administrator'; Id = '7698a772-787b-4ac8-901f-60d6b08affd2' },
22+
@{ Name = 'Teams Administrator'; Id = '69091246-20e8-4a56-aa4d-066075b2a7a8' },
23+
@{ Name = 'SharePoint Administrator'; Id = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' },
24+
@{ Name = 'Authentication Policy Administrator'; Id = '0526716b-113d-4c15-b2c8-68e3c22b9f80' },
25+
@{ Name = 'Privileged Role Administrator'; Id = 'e8611ab8-c189-46e8-94e1-60213ab1f814' },
26+
@{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' },
27+
@{ Name = 'Billing Administrator'; Id = 'b0f54661-2d74-4c50-afa3-1ec803f12efe'; Optional = $true },
28+
@{ Name = 'Global Reader'; Id = 'f2ef992c-3afb-46b9-b7cf-a126ee74c451'; Optional = $true },
29+
@{ Name = 'Domain Name Administrator'; Id = '8329153b-31d0-4727-b945-745eb3bc5f31'; Optional = $true }
30+
)
31+
32+
try {
33+
$Tenant = Get-Tenants -TenantFilter $TenantFilter -IncludeErrors
34+
if (-not $Tenant) {
35+
return
36+
}
37+
$TenantId = $Tenant.customerId
38+
$Issues = [System.Collections.Generic.List[object]]::new()
39+
40+
# Test Graph API connectivity and GDAP role assignments
41+
$GraphStatus = $false
42+
$GraphMessage = ''
43+
try {
44+
$BulkRequests = $ExpectedRoles | ForEach-Object {
45+
@{
46+
id = "roleManagement_$($_.Id)"
47+
method = 'GET'
48+
url = "roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '$($_.Id)'&`$expand=principal"
49+
}
50+
}
51+
$GDAPRolesGraph = New-GraphBulkRequest -tenantid $TenantId -Requests $BulkRequests
52+
$MissingRoles = [System.Collections.Generic.List[string]]::new()
53+
54+
foreach ($RoleId in $ExpectedRoles) {
55+
$GraphRole = $GDAPRolesGraph.body.value | Where-Object -Property roleDefinitionId -EQ $RoleId.Id
56+
$Role = $GraphRole.principal | Where-Object -Property organizationId -EQ $env:TenantID
57+
if (-not $Role -and $RoleId.Optional -ne $true) {
58+
$MissingRoles.Add($RoleId.Name)
59+
}
60+
}
61+
62+
$GraphStatus = $true
63+
if ($MissingRoles.Count -gt 0) {
64+
$GraphMessage = "Graph connected but missing required GDAP roles: $($MissingRoles -join ', ')"
65+
$Issues.Add([PSCustomObject]@{
66+
Issue = 'MissingGDAPRoles'
67+
Message = $GraphMessage
68+
MissingRoles = ($MissingRoles -join ', ')
69+
Tenant = $TenantFilter
70+
})
71+
}
72+
} catch {
73+
$ErrorMessage = Get-NormalizedError -message $_.Exception.Message
74+
$GraphMessage = "Failed to connect to Graph API: $ErrorMessage"
75+
$Issues.Add([PSCustomObject]@{
76+
Issue = 'GraphFailure'
77+
Message = $GraphMessage
78+
Tenant = $TenantFilter
79+
})
80+
}
81+
82+
# Test Exchange Online connectivity
83+
$ExchangeStatus = $false
84+
try {
85+
$null = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-OrganizationConfig' -ErrorAction Stop
86+
$ExchangeStatus = $true
87+
} catch {
88+
$ErrorMessage = Get-NormalizedError -message $_.Exception.Message
89+
$Issues.Add([PSCustomObject]@{
90+
Issue = 'ExchangeFailure'
91+
Message = "Failed to connect to Exchange Online: $ErrorMessage"
92+
Tenant = $TenantFilter
93+
})
94+
}
95+
96+
# Build alert data only if there are issues
97+
$AlertData = @()
98+
if (-not $GraphStatus -and -not $ExchangeStatus) {
99+
$AlertData = @([PSCustomObject]@{
100+
Message = "Tenant $TenantFilter is inaccessible. Graph API and Exchange Online connectivity both failed. This tenant may have removed GDAP permissions or requires consent refresh."
101+
GraphStatus = $false
102+
ExchangeStatus = $false
103+
Issues = ($Issues | ForEach-Object { $_.Message }) -join '; '
104+
Tenant = $TenantFilter
105+
})
106+
} elseif (-not $GraphStatus) {
107+
$AlertData = @([PSCustomObject]@{
108+
Message = "Tenant $TenantFilter has lost Graph API access. $GraphMessage"
109+
GraphStatus = $false
110+
ExchangeStatus = $ExchangeStatus
111+
Issues = ($Issues | ForEach-Object { $_.Message }) -join '; '
112+
Tenant = $TenantFilter
113+
})
114+
} elseif (-not $ExchangeStatus) {
115+
$AlertData = @([PSCustomObject]@{
116+
Message = "Tenant $TenantFilter has lost Exchange Online access. This may indicate missing Exchange Administrator GDAP role or removed consent."
117+
GraphStatus = $GraphStatus
118+
ExchangeStatus = $false
119+
Issues = ($Issues | ForEach-Object { $_.Message }) -join '; '
120+
Tenant = $TenantFilter
121+
})
122+
} elseif ($MissingRoles.Count -gt 0) {
123+
$AlertData = @([PSCustomObject]@{
124+
Message = "Tenant $TenantFilter is accessible but missing required GDAP roles: $($MissingRoles -join ', '). This may indicate a CIPP-SAM permission update is needed."
125+
GraphStatus = $GraphStatus
126+
ExchangeStatus = $ExchangeStatus
127+
MissingRoles = ($MissingRoles -join ', ')
128+
Tenant = $TenantFilter
129+
})
130+
}
131+
132+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
133+
} catch {
134+
Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Tenant access alert error for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.Message)" -sev Error
135+
}
136+
}

0 commit comments

Comments
 (0)