Skip to content

Commit e4a431e

Browse files
Merge pull request KelvinTegelaar#1879 from KelvinTegelaar/dev
Dev to release
2 parents fa00351 + 1d05a9b commit e4a431e

File tree

112 files changed

+2686
-403
lines changed

Some content is hidden

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

112 files changed

+2686
-403
lines changed

CIPPHttpTrigger/function.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
"name": "starter",
2727
"type": "durableClient",
2828
"direction": "in"
29+
},
30+
{
31+
"type": "queue",
32+
"direction": "out",
33+
"name": "QueueItem",
34+
"queueName": "cippqueue"
2935
}
3036
]
3137
}

CIPPQueueTrigger/function.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"scriptFile": "../Modules/CippEntrypoints/CippEntrypoints.psm1",
3+
"entryPoint": "Receive-CippQueueTrigger",
4+
"bindings": [
5+
{
6+
"name": "QueueItem",
7+
"type": "queueTrigger",
8+
"direction": "in",
9+
"queueName": "cippqueue"
10+
},
11+
{
12+
"name": "starter",
13+
"type": "durableClient",
14+
"direction": "in"
15+
}
16+
]
17+
}

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

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,63 @@ function Get-CIPPAlertMFAAdmins {
1818
}
1919
}
2020
if (!$DuoActive) {
21-
$Users = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq true and isMfaRegistered eq false and userType eq 'member'&`$select=id,userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true |
22-
Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' }
21+
$MFAReport = try { Get-CIPPMFAStateReport -TenantFilter $TenantFilter } catch { $null }
22+
$IncludeDisabled = [System.Convert]::ToBoolean($InputValue)
2323

24-
# Filter out JIT admins if any users were found
25-
if ($Users) {
24+
# Check 1: Admins with no MFA registered — prefer cache, fall back to live Graph
25+
$Users = if ($MFAReport) {
26+
$MFAReport | Where-Object { $_.IsAdmin -eq $true -and $_.MFARegistration -eq $false -and ($IncludeDisabled -or $_.AccountEnabled -eq $true) }
27+
} else {
28+
New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq true and isMfaRegistered eq false and userType eq 'member'&`$select=id,userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true |
29+
Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' } |
30+
Select-Object @{n = 'ID'; e = { $_.id } }, @{n = 'UPN'; e = { $_.userPrincipalName } }, @{n = 'DisplayName'; e = { $_.userDisplayName } }
31+
}
32+
33+
# Check 2: Admins with MFA registered but no enforcement.
34+
# I hate how this ended up looking, but I couldn't think of a better way to do it ¯\_(ツ)_/¯
35+
$UnenforcedAdmins = $MFAReport | Where-Object {
36+
$_.IsAdmin -eq $true -and
37+
$_.MFARegistration -eq $true -and
38+
($IncludeDisabled -or $_.AccountEnabled -eq $true) -and
39+
$_.PerUser -notin @('Enforced', 'Enabled') -and
40+
$null -ne $_.CoveredBySD -and
41+
$_.CoveredBySD -ne $true -and
42+
$_.CoveredByCA -notlike 'Enforced*'
43+
}
44+
45+
# Filter out JIT admins
46+
if ($Users -or $UnenforcedAdmins) {
2647
$Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1
2748
$JITAdmins = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true" -tenantid $TenantFilter -ComplexFilter
2849
$JITAdminIds = $JITAdmins.id
29-
$Users = $Users | Where-Object { $_.id -notin $JITAdminIds }
50+
$Users = $Users | Where-Object { $_.ID -notin $JITAdminIds }
51+
$UnenforcedAdmins = $UnenforcedAdmins | Where-Object { $_.ID -notin $JITAdminIds }
52+
}
53+
54+
$AlertData = [System.Collections.Generic.List[PSCustomObject]]::new()
55+
56+
foreach ($user in $Users) {
57+
$AlertData.Add([PSCustomObject]@{
58+
Message = "Admin user $($user.DisplayName) ($($user.UPN)) does not have MFA registered."
59+
UserPrincipalName = $user.UPN
60+
DisplayName = $user.DisplayName
61+
Id = $user.ID
62+
Tenant = $TenantFilter
63+
})
3064
}
3165

32-
if ($Users.UserPrincipalName) {
33-
$AlertData = foreach ($user in $Users) {
34-
[PSCustomObject]@{
35-
Message = "Admin user $($user.userDisplayName) ($($user.userPrincipalName)) does not have MFA registered."
36-
UserPrincipalName = $user.userPrincipalName
37-
DisplayName = $user.userDisplayName
38-
Id = $user.id
39-
LastUpdated = $user.lastUpdatedDateTime
66+
foreach ($user in $UnenforcedAdmins) {
67+
$AlertData.Add([PSCustomObject]@{
68+
Message = "Admin user $($user.DisplayName) ($($user.UPN)) has MFA registered but no enforcement method (Per-User MFA, Security Defaults, or Conditional Access) is active."
69+
UserPrincipalName = $user.UPN
70+
DisplayName = $user.DisplayName
71+
Id = $user.ID
4072
Tenant = $TenantFilter
41-
}
42-
}
73+
})
74+
}
4375

76+
if ($AlertData.Count -gt 0) {
4477
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
45-
4678
}
4779
} else {
4880
Write-LogMessage -message 'Potentially using Duo for MFA, could not check MFA status for Admins with 100% accuracy' -API 'MFA Alerts - Informational' -tenant $TenantFilter -sev Info

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ function Get-CIPPAlertMXRecordChanged {
1616
$CacheTable = Get-CippTable -tablename 'CacheMxRecords'
1717
$PreviousResults = Get-CIPPAzDataTableEntity @CacheTable -Filter "PartitionKey eq '$TenantFilter'"
1818

19+
if (!$DomainData) {
20+
return
21+
}
22+
1923
$ChangedDomains = foreach ($Domain in $DomainData) {
2024
try {
2125
$PreviousDomain = $PreviousResults | Where-Object { $_.Domain -eq $Domain.Domain }
@@ -60,8 +64,6 @@ function Get-CIPPAlertMXRecordChanged {
6064
if ($ChangedDomains) {
6165
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains
6266
}
63-
return $true
64-
6567
} catch {
6668
Write-LogMessage -message "Failed to check MX record changes: $($_.Exception.Message)" -API 'MX Record Alert' -tenant $TenantFilter -sev Error
6769
}

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,30 @@ function Get-CIPPAlertNewMFADevice {
2020
$User = $Log.targetResources[0].userPrincipalName
2121
if (-not $User) { $User = $Log.initiatedBy.user.userPrincipalName }
2222

23+
$IPAddress = $Log.initiatedBy.user.ipAddress
24+
$LocationData = $null
25+
if (-not [string]::IsNullOrEmpty($IPAddress) -and $IPAddress -notmatch '[X]+') {
26+
try {
27+
$LocationData = Get-CIPPGeoIPLocation -IP $IPAddress
28+
} catch {
29+
Write-Information "Could not enrich MFA audit IP ${$IPAddress}: $($_.Exception.Message)"
30+
}
31+
}
32+
2333
[PSCustomObject]@{
24-
Message = "New MFA method registered: $User"
25-
User = $User
26-
DisplayName = $Log.targetResources[0].displayName
27-
Activity = $Log.activityDisplayName
28-
ActivityTime = $Log.activityDateTime
29-
Tenant = $TenantFilter
34+
Message = "New MFA method registered: $User"
35+
User = $User
36+
DisplayName = $Log.targetResources[0].displayName
37+
Activity = $Log.activityDisplayName
38+
ActivityTime = $Log.activityDateTime
39+
Tenant = $TenantFilter
40+
IpAddress = $IPAddress
41+
CountryOrRegion = if ($LocationData) { $LocationData.countryCode } else { $null }
42+
City = if ($LocationData) { $LocationData.city } else { $null }
43+
Proxy = if ($LocationData) { $LocationData.proxy } else { $null }
44+
Hosting = if ($LocationData) { $LocationData.hosting } else { $null }
45+
ASN = if ($LocationData) { $LocationData.asname } else { $null }
46+
GeoLocationInfo = if ($LocationData) { ($LocationData | ConvertTo-Json -Depth 10 -Compress) } else { $null }
3047
}
3148
}
3249
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function Get-CIPPAlertSmtpAuthSuccess {
1313

1414
try {
1515
# Graph API endpoint for sign-ins
16-
$uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=clientAppUsed eq 'Authenticated SMTP' and status/errorCode eq 0"
16+
$uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=(clientAppUsed eq 'Authenticated SMTP' or clientAppUsed eq 'SMTP') and status/errorCode eq 0"
1717

1818
# Call Graph API for the given tenant
1919
$SignIns = New-GraphGetRequest -uri $uri -tenantid $TenantFilter
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
function Compare-CIPPIntuneAssignments {
2+
<#
3+
.SYNOPSIS
4+
Compares existing Intune policy assignments against expected assignment settings.
5+
.DESCRIPTION
6+
Returns $true if the existing assignments match the expected settings, $false if they differ,
7+
or $null if the comparison could not be completed (e.g. Graph error).
8+
.PARAMETER ExistingAssignments
9+
The current assignments on the policy, as returned by Get-CIPPIntunePolicyAssignments.
10+
.PARAMETER ExpectedAssignTo
11+
The expected assignment target type: allLicensedUsers, AllDevices, AllDevicesAndUsers,
12+
customGroup, or On (no assignment).
13+
.PARAMETER ExpectedCustomGroup
14+
The expected custom group name(s), comma-separated. Used when ExpectedAssignTo is 'customGroup'.
15+
.PARAMETER ExpectedExcludeGroup
16+
The expected exclusion group name(s), comma-separated.
17+
.PARAMETER ExpectedAssignmentFilter
18+
The expected assignment filter display name. Wildcards supported.
19+
.PARAMETER ExpectedAssignmentFilterType
20+
'include' or 'exclude'. Defaults to 'include'.
21+
.PARAMETER TenantFilter
22+
The tenant to query for group/filter resolution.
23+
.FUNCTIONALITY
24+
Internal
25+
#>
26+
param(
27+
[object[]]$ExistingAssignments,
28+
[string]$ExpectedAssignTo,
29+
[string]$ExpectedCustomGroup,
30+
[string]$ExpectedExcludeGroup,
31+
[string]$ExpectedAssignmentFilter,
32+
[string]$ExpectedAssignmentFilterType = 'include',
33+
[Parameter(Mandatory = $true)]
34+
[string]$TenantFilter
35+
)
36+
37+
try {
38+
# Normalize existing targets
39+
$ExistingTargetTypes = @($ExistingAssignments.target.'@odata.type' | Where-Object { $_ })
40+
$ExistingIncludeGroupIds = @(
41+
$ExistingAssignments |
42+
Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' } |
43+
ForEach-Object { $_.target.groupId }
44+
)
45+
$ExistingExcludeGroupIds = @(
46+
$ExistingAssignments |
47+
Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget' } |
48+
ForEach-Object { $_.target.groupId }
49+
)
50+
51+
# Determine expected include target types
52+
$ExpectedIncludeTypes = switch ($ExpectedAssignTo) {
53+
'allLicensedUsers' { @('#microsoft.graph.allLicensedUsersAssignmentTarget') }
54+
'AllDevices' { @('#microsoft.graph.allDevicesAssignmentTarget') }
55+
'AllDevicesAndUsers' { @('#microsoft.graph.allDevicesAssignmentTarget', '#microsoft.graph.allLicensedUsersAssignmentTarget') }
56+
'customGroup' { @('#microsoft.graph.groupAssignmentTarget') }
57+
'On' { @() }
58+
default { @() }
59+
}
60+
61+
# Compare include target types (ignore exclusion targets)
62+
$ExistingIncludeTypes = @($ExistingTargetTypes | Where-Object { $_ -ne '#microsoft.graph.exclusionGroupAssignmentTarget' })
63+
$TargetTypeMatch = $true
64+
foreach ($t in $ExpectedIncludeTypes) {
65+
if ($t -notin $ExistingIncludeTypes) { $TargetTypeMatch = $false; break }
66+
}
67+
if ($TargetTypeMatch) {
68+
foreach ($t in $ExistingIncludeTypes) {
69+
if ($t -notin $ExpectedIncludeTypes) { $TargetTypeMatch = $false; break }
70+
}
71+
}
72+
73+
# Lazy-load groups cache only if needed
74+
$AllGroupsCache = $null
75+
76+
# For custom groups, resolve names to IDs and compare
77+
$IncludeGroupMatch = $true
78+
if ($ExpectedAssignTo -eq 'customGroup' -and $ExpectedCustomGroup) {
79+
$AllGroupsCache = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter
80+
$ExpectedGroupIds = @(
81+
$ExpectedCustomGroup.Split(',').Trim() | ForEach-Object {
82+
$name = $_
83+
$AllGroupsCache | Where-Object { $_.displayName -like $name } | Select-Object -ExpandProperty id
84+
} | Where-Object { $_ }
85+
)
86+
$MissingIds = @($ExpectedGroupIds | Where-Object { $_ -notin $ExistingIncludeGroupIds })
87+
$ExtraIds = @($ExistingIncludeGroupIds | Where-Object { $_ -notin $ExpectedGroupIds })
88+
$IncludeGroupMatch = ($MissingIds.Count -eq 0 -and $ExtraIds.Count -eq 0)
89+
}
90+
91+
# Compare exclusion groups
92+
$ExcludeGroupMatch = $true
93+
if ($ExpectedExcludeGroup) {
94+
if (-not $AllGroupsCache) {
95+
$AllGroupsCache = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter
96+
}
97+
$ExpectedExcludeIds = @(
98+
$ExpectedExcludeGroup.Split(',').Trim() | ForEach-Object {
99+
$name = $_
100+
$AllGroupsCache | Where-Object { $_.displayName -like $name } | Select-Object -ExpandProperty id
101+
} | Where-Object { $_ }
102+
)
103+
$MissingExcludeIds = @($ExpectedExcludeIds | Where-Object { $_ -notin $ExistingExcludeGroupIds })
104+
$ExtraExcludeIds = @($ExistingExcludeGroupIds | Where-Object { $_ -notin $ExpectedExcludeIds })
105+
$ExcludeGroupMatch = ($MissingExcludeIds.Count -eq 0 -and $ExtraExcludeIds.Count -eq 0)
106+
} elseif ($ExistingExcludeGroupIds.Count -gt 0) {
107+
# No exclusions expected but some exist
108+
$ExcludeGroupMatch = $false
109+
}
110+
111+
# Compare assignment filter
112+
$FilterMatch = $true
113+
if ($ExpectedAssignmentFilter) {
114+
$ExistingFilterIds = @(
115+
$ExistingAssignments |
116+
Where-Object { $_.target.deviceAndAppManagementAssignmentFilterId } |
117+
ForEach-Object { $_.target.deviceAndAppManagementAssignmentFilterId }
118+
)
119+
if ($ExistingFilterIds.Count -eq 0) {
120+
$FilterMatch = $false
121+
} else {
122+
$AllFilters = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters' -tenantid $TenantFilter
123+
$ExpectedFilter = $AllFilters | Where-Object { $_.displayName -like $ExpectedAssignmentFilter } | Select-Object -First 1
124+
$FilterMatch = $ExpectedFilter -and ($ExpectedFilter.id -in $ExistingFilterIds)
125+
}
126+
}
127+
128+
return $TargetTypeMatch -and $IncludeGroupMatch -and $ExcludeGroupMatch -and $FilterMatch
129+
130+
} catch {
131+
Write-Warning "Compare-CIPPIntuneAssignments failed for tenant $TenantFilter : $($_.Exception.Message)"
132+
return $null # null = unknown, don't treat as mismatch
133+
}
134+
}

0 commit comments

Comments
 (0)