Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0a43c2d
Replace New-CippDbRequest with New-ExoRequest
TecharyJames Feb 25, 2026
b877566
Fix: Update GDAP relationship check to use 15-role recommended group set
Copilot Feb 25, 2026
de9eab1
Merge pull request #1847 from Zacgoose/Update-GDAP-relationship-check
KelvinTegelaar Feb 25, 2026
b3a0bc3
Merge pull request #1846 from TecharyJames/disableSMTPAuth-fix
KelvinTegelaar Feb 25, 2026
b51a860
Normalize licenses and batch user lookups
Zacgoose Feb 25, 2026
3c7bb47
Merge pull request #1848 from Zacgoose/licence-assignment-fixes
KelvinTegelaar Feb 25, 2026
bbd8575
Handle defaultDomainName when managing defaults
Zacgoose Feb 26, 2026
43b7b3c
change standards to run every 12 hours
JohnDuprey Feb 26, 2026
76df64c
Merge pull request #1849 from Zacgoose/tenant-offboarding-defaults
KelvinTegelaar Feb 26, 2026
db0521c
add group type to membership change
JohnDuprey Feb 26, 2026
c5e561c
fix compares for #5477
KelvinTegelaar Feb 26, 2026
90aaa3d
add alertcomment to payload
KelvinTegelaar Feb 26, 2026
578bf70
auditlog rentention cleanup speed and rerun protection
KelvinTegelaar Feb 26, 2026
c7d872a
updated log retention
KelvinTegelaar Feb 26, 2026
eb0e683
feat: Add WindowsBackupRestore standard for Intune WBfO enrollment co…
kris6673 Feb 26, 2026
8f743a8
Merge pull request #1850 from kris6673/feature/standard-windows-backu…
KelvinTegelaar Feb 26, 2026
d0d2595
Enhance Teams federation config parsing and updates
JohnDuprey Feb 26, 2026
829afc9
Improve Teams federation domains parsing and validation
JohnDuprey Feb 26, 2026
b2a4599
Add mailboxes report API and use-report flag
JohnDuprey Feb 26, 2026
edb3535
fix permission on contact templates
JohnDuprey Feb 26, 2026
547599b
Use raw SkuPartNumber for license names
JohnDuprey Feb 27, 2026
01aa9b5
fix: offboarding job conditions
JohnDuprey Feb 27, 2026
34ec15b
Add RouteMessageOutboundConnector support
Zacgoose Feb 27, 2026
ee2d048
Merge pull request #1851 from Zacgoose/transport-rule-fixes
KelvinTegelaar Feb 27, 2026
db91dd0
fixes bug weith adding removing locations.
KelvinTegelaar Feb 27, 2026
d91ef1a
Fix comparison object
Zacgoose Feb 27, 2026
e87d5e8
fix: Update role in Invoke-ExecDnsConfig.ps1
Jr7468 Feb 27, 2026
237467e
fixes ordered sets in intune policies
KelvinTegelaar Feb 27, 2026
1186fd6
nuked source
KelvinTegelaar Feb 27, 2026
95eebd2
Merge pull request #1853 from Jr7468/dev
KelvinTegelaar Feb 27, 2026
cd45e78
Mark listStandardTemplates as AnyTenant
JohnDuprey Feb 27, 2026
81b46f2
fix: Handle SplitOverProps JSON errors and cleanup
JohnDuprey Feb 27, 2026
cd2d86a
fix: Record permission update status and adjust retry logic
JohnDuprey Feb 27, 2026
b5af99c
fix: filter down to UserPrincipalName to limit object size
JohnDuprey Feb 27, 2026
e6f3c6a
bump version
JohnDuprey Feb 27, 2026
b85461e
Merge pull request #1852 from Zacgoose/fix-retention-tag-standard
JohnDuprey Feb 27, 2026
fa00351
Merge pull request #1854 from KelvinTegelaar/dev
JohnDuprey Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CIPPTimers.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"Id": "9b0c8e50-f798-49db-9a8b-dbcc0fcadeea",
"Command": "Start-StandardsOrchestrator",
"Description": "Orchestrator to process standards",
"Cron": "0 0 */4 * * *",
"Cron": "0 0 */12 * * *",
"Priority": 4,
"RunOnProcessor": true,
"PreferredProcessor": "standards"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function Get-CIPPAlertGroupMembershipChange {
$AuditLogs = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=activityDateTime ge $OneHourAgo and (activityDisplayName eq 'Add member to group' or activityDisplayName eq 'Remove member from group')" -tenantid $TenantFilter

$AlertData = foreach ($Log in $AuditLogs) {
$Member = ($Log.targetResources | Where-Object { $_.type -in @('User', 'ServicePrincipal') })[0]
$Member = ($Log.targetResources | Where-Object { $_.type -in @('User', 'ServicePrincipal', 'Group') })[0]
$GroupProp = ($Member.modifiedProperties | Where-Object { $_.displayName -eq 'Group.DisplayName' })
$GroupDisplayName = (($GroupProp.newValue ?? $GroupProp.oldValue) -replace '"', '')
if (!$GroupDisplayName -or !($MonitoredGroups | Where-Object { $GroupDisplayName -like $_ })) { continue }
Expand Down
107 changes: 90 additions & 17 deletions Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ function Compare-CIPPIntuneObject {
'agents',
'isSynced'
'locationInfo',
'templateId'
'templateId',
'source'
)

$excludeProps = $defaultExcludeProperties + $ExcludeProperties
Expand All @@ -49,6 +50,38 @@ function Compare-CIPPIntuneObject {
$excludeProps -contains $PropertyName)
}

function ShouldCompareAsUnorderedSet {
param (
[string]$PropertyPath
)
# Properties that should be compared as sets (order doesn't matter)
$unorderedSetPatterns = @(
'includeGroups',
'excludeGroups',
'includeUsers',
'excludeUsers',
'includeApplications',
'excludeApplications',
'includeRoles',
'excludeRoles',
'includePlatforms',
'excludePlatforms',
'includeLocations',
'excludeLocations',
'includeDevices',
'excludeDevices',
'includeGuestOrExternalUserTypes',
'excludeGuestOrExternalUserTypes'
)

foreach ($pattern in $unorderedSetPatterns) {
if ($PropertyPath -match "\.$pattern(\.\d+)?$" -or $PropertyPath -eq $pattern) {
return $true
}
}
return $false
}

function Compare-ObjectsRecursively {
param (
[Parameter(Mandatory = $true)]
Expand Down Expand Up @@ -136,25 +169,65 @@ function Compare-CIPPIntuneObject {
}
}
} elseif ($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) {
$maxLength = [Math]::Max($Object1.Count, $Object2.Count)

for ($i = 0; $i -lt $maxLength; $i++) {
$newPath = "$PropertyPath.$i"

if ($i -lt $Object1.Count -and $i -lt $Object2.Count) {
Compare-ObjectsRecursively -Object1 $Object1[$i] -Object2 $Object2[$i] -PropertyPath $newPath -Depth ($Depth + 1) -MaxDepth $MaxDepth
} elseif ($i -lt $Object1.Count) {
# Check if this array should be compared as an unordered set
if (ShouldCompareAsUnorderedSet -PropertyPath $PropertyPath) {
# For unordered sets, compare contents regardless of order
if ($Object1.Count -ne $Object2.Count) {
# Different lengths - report the difference
$result.Add([PSCustomObject]@{
Property = $newPath
ExpectedValue = $Object1[$i]
ReceivedValue = ''
Property = $PropertyPath
ExpectedValue = "Array with $($Object1.Count) items"
ReceivedValue = "Array with $($Object2.Count) items"
})
} else {
$result.Add([PSCustomObject]@{
Property = $newPath
ExpectedValue = ''
ReceivedValue = $Object2[$i]
})
# Same length - check if all items exist in both arrays
$array1Sorted = @($Object1 | Sort-Object)
$array2Sorted = @($Object2 | Sort-Object)

for ($i = 0; $i -lt $array1Sorted.Count; $i++) {
$item1 = $array1Sorted[$i]
$item2 = $array2Sorted[$i]

# For primitive types, direct comparison
if ($item1 -is [string] -or $item1 -is [int] -or $item1 -is [long] -or $item1 -is [bool] -or $item1 -is [double] -or $item1 -is [decimal]) {
if ($item1 -ne $item2) {
# Items don't match even after sorting - arrays have different contents
$result.Add([PSCustomObject]@{
Property = $PropertyPath
ExpectedValue = ($Object1 -join ', ')
ReceivedValue = ($Object2 -join ', ')
})
break
}
} else {
# For complex objects, recursively compare with a generic path
$newPath = "$PropertyPath[$i]"
Compare-ObjectsRecursively -Object1 $item1 -Object2 $item2 -PropertyPath $newPath -Depth ($Depth + 1) -MaxDepth $MaxDepth
}
}
}
} else {
# Ordered array comparison (original behavior)
$maxLength = [Math]::Max($Object1.Count, $Object2.Count)

for ($i = 0; $i -lt $maxLength; $i++) {
$newPath = "$PropertyPath.$i"

if ($i -lt $Object1.Count -and $i -lt $Object2.Count) {
Compare-ObjectsRecursively -Object1 $Object1[$i] -Object2 $Object2[$i] -PropertyPath $newPath -Depth ($Depth + 1) -MaxDepth $MaxDepth
} elseif ($i -lt $Object1.Count) {
$result.Add([PSCustomObject]@{
Property = $newPath
ExpectedValue = $Object1[$i]
ReceivedValue = ''
})
} else {
$result.Add([PSCustomObject]@{
Property = $newPath
ExpectedValue = ''
ReceivedValue = $Object2[$i]
})
}
}
}
} elseif ($Object1 -is [PSCustomObject] -or $Object1.PSObject.Properties.Count -gt 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,24 @@ function Push-UpdatePermissionsQueue {
$DomainRefreshRequired = $true
}
Write-Information 'Updating permissions'
Add-CIPPApplicationPermission -RequiredResourceAccess 'CIPPDefaults' -ApplicationId $env:ApplicationID -tenantfilter $Item.customerId
Add-CIPPDelegatedPermission -RequiredResourceAccess 'CIPPDefaults' -ApplicationId $env:ApplicationID -tenantfilter $Item.customerId
Write-LogMessage -tenant $Item.defaultDomainName -tenantId $Item.customerId -message "Updated permissions for $($Item.displayName)" -Sev 'Info' -API 'UpdatePermissionsQueue'
$AppResults = Add-CIPPApplicationPermission -RequiredResourceAccess 'CIPPDefaults' -ApplicationId $env:ApplicationID -tenantfilter $Item.customerId
$DelegatedResults = Add-CIPPDelegatedPermission -RequiredResourceAccess 'CIPPDefaults' -ApplicationId $env:ApplicationID -tenantfilter $Item.customerId

# Check for permission failures (excluding service principal creation failures)
$AllResults = @($AppResults) + @($DelegatedResults)
$PermissionFailures = $AllResults | Where-Object {
$_ -like '*Failed*' -and
$_ -notlike '*Failed to create service principal*'
}

if ($PermissionFailures) {
$Status = 'Failed'
$FailureMessage = ($PermissionFailures -join '; ')
Write-LogMessage -tenant $Item.defaultDomainName -tenantId $Item.customerId -message "Permission update completed with failures for $($Item.displayName): $FailureMessage" -Sev 'Warn' -API 'UpdatePermissionsQueue'
} else {
$Status = 'Success'
Write-LogMessage -tenant $Item.defaultDomainName -tenantId $Item.customerId -message "Updated permissions for $($Item.displayName)" -Sev 'Info' -API 'UpdatePermissionsQueue'
}

if ($Item.defaultDomainName -ne 'PartnerTenant') {
Write-Information 'Pushing CIPP-SAM admin roles'
Expand All @@ -38,11 +53,15 @@ function Push-UpdatePermissionsQueue {
$unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
$GraphRequest = @{
LastApply = "$unixtime"
LastStatus = "$Status"
applicationId = "$($env:ApplicationID)"
Tenant = "$($Item.customerId)"
PartitionKey = 'Tenant'
RowKey = "$($Item.customerId)"
}
if ($PermissionFailures) {
$GraphRequest.LastError = $FailureMessage
}
Add-CIPPAzDataTableEntity @Table -Entity $GraphRequest -Force

if ($DomainRefreshRequired) {
Expand All @@ -53,5 +72,6 @@ function Push-UpdatePermissionsQueue {
}
} catch {
Write-Information "Error updating permissions for $($Item.displayName)"
Write-LogMessage -tenant $Item.defaultDomainName -tenantId $Item.customerId -message "Error updating permissions for $($Item.displayName) - $($_.Exception.Message)" -Sev 'Error' -API 'UpdatePermissionsQueue'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function Invoke-ExecDnsConfig {
.FUNCTIONALITY
Entrypoint
.ROLE
CIPP.AppSettings.ReadWrite
Tenant.Domains.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
Function Invoke-ListContactTemplates {
function Invoke-ListContactTemplates {
<#
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
Exchange.Read
Exchange.Contact.Read
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
Expand Down Expand Up @@ -39,9 +39,9 @@ Function Invoke-ListContactTemplates {
if (-not $Templates) {
Write-LogMessage -headers $Headers -API $APIName -message "Template with ID $RequestedID not found" -sev 'Warn'
return ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::NotFound
Body = @{ Error = "Template with ID $RequestedID not found" }
})
StatusCode = [HttpStatusCode]::NotFound
Body = @{ Error = "Template with ID $RequestedID not found" }
})
return
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,27 @@ function Invoke-ListMailboxes {
param($Request, $TriggerMetadata)
# Interact with query parameters or the body of the request.
$TenantFilter = $Request.Query.tenantFilter
$UseReportDB = $Request.Query.UseReportDB

try {
# If UseReportDB is specified, retrieve from report database
if ($UseReportDB -eq 'true') {
try {
$GraphRequest = Get-CIPPMailboxesReport -TenantFilter $TenantFilter -ErrorAction Stop
$StatusCode = [HttpStatusCode]::OK
} catch {
Write-Host "Error retrieving mailboxes from report database: $($_.Exception.Message)"
$StatusCode = [HttpStatusCode]::InternalServerError
$GraphRequest = $_.Exception.Message
}

return ([HttpResponseContext]@{
StatusCode = $StatusCode
Body = @($GraphRequest)
})
}

# Original live EXO logic
$Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,PersistedCapabilities,LitigationHoldEnabled,LitigationHoldDate,LitigationHoldDuration,ComplianceTagHoldApplied,RetentionHoldEnabled,InPlaceHolds,RetentionPolicy'
$ExoRequest = @{
tenantid = $TenantFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ function Invoke-AddEditTransportRule {
$DeleteMessage = $Request.Body.DeleteMessage
$Quarantine = $Request.Body.Quarantine
$RedirectMessageTo = $Request.Body.RedirectMessageTo
$RouteMessageOutboundConnector = $Request.Body.RouteMessageOutboundConnector
$BlindCopyTo = $Request.Body.BlindCopyTo
$CopyTo = $Request.Body.CopyTo
$ModerateMessageByUser = $Request.Body.ModerateMessageByUser
Expand Down Expand Up @@ -436,6 +437,7 @@ function Invoke-AddEditTransportRule {
if ($null -ne $RedirectMessageTo -and $RedirectMessageTo.Count -gt 0) {
$ruleParams.Add('RedirectMessageTo', $RedirectMessageTo)
}
if ($null -ne$RouteMessageOutboundConnector) {$ruleParams.Add('RouteMessageOutboundConnector', $RouteMessageOutboundConnector)}
if ($null -ne $BlindCopyTo -and $BlindCopyTo.Count -gt 0) { $ruleParams.Add('BlindCopyTo', $BlindCopyTo) }
if ($null -ne $CopyTo -and $CopyTo.Count -gt 0) { $ruleParams.Add('CopyTo', $CopyTo) }
if ($null -ne $ModerateMessageByUser -and $ModerateMessageByUser.Count -gt 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ function Invoke-CIPPOffboardingJob {
}
}
@{
Condition = { ![string]::IsNullOrEmpty($Options.OnedriveAccess) }
Condition = { $Options.OnedriveAccess.Count -gt 0 }
Cmdlet = 'Set-CIPPSharePointPerms'
Parameters = @{
tenantFilter = $TenantFilter
Expand All @@ -166,7 +166,7 @@ function Invoke-CIPPOffboardingJob {
}
}
@{
Condition = { ![string]::IsNullOrEmpty($Options.AccessNoAutomap) }
Condition = { $Options.AccessNoAutomap.Count -gt 0 }
Cmdlet = 'Set-CIPPMailboxAccess'
Parameters = @{
tenantFilter = $TenantFilter
Expand All @@ -179,7 +179,7 @@ function Invoke-CIPPOffboardingJob {
}
}
@{
Condition = { ![string]::IsNullOrEmpty($Options.AccessAutomap) }
Condition = { $Options.AccessAutomap.Count -gt 0 }
Cmdlet = 'Set-CIPPMailboxAccess'
Parameters = @{
tenantFilter = $TenantFilter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,10 @@ function Invoke-EditUser {
value = 'Set-CIPPUserLicense'
}
Parameters = [pscustomobject]@{
UserId = $UserObj.id
APIName = 'Sherweb License Assignment'
AddLicenses = $licenses
UserId = $UserObj.id
APIName = 'Sherweb License Assignment'
AddLicenses = $licenses
UserPrincipalName = $UserPrincipalName
}
ScheduledTime = 0 #right now, which is in the next 15 minutes and should cover most cases.
PostExecution = @{
Expand All @@ -124,12 +125,12 @@ function Invoke-EditUser {
$Results.Add( 'Success. User license is already correct.' )
} else {
if ($UserObj.removeLicenses) {
$licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $CurrentLicenses.assignedLicenses.skuId -Headers $Headers -APIName $APIName
$licResults = Set-CIPPUserLicense -UserPrincipalName $UserPrincipalName -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $CurrentLicenses.assignedLicenses.skuId -Headers $Headers -APIName $APIName
$Results.Add($licResults)
} else {
#Remove all objects from $CurrentLicenses.assignedLicenses.skuId that are in $licenses
$RemoveLicenses = $CurrentLicenses.assignedLicenses.skuId | Where-Object { $_ -notin $licenses }
$licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $RemoveLicenses -AddLicenses $licenses -Headers $Headers -APIName $APIName
$licResults = Set-CIPPUserLicense -UserPrincipalName $UserPrincipalName -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $RemoveLicenses -AddLicenses $licenses -Headers $Headers -APIName $APIName
$Results.Add($licResults)
}

Expand Down
Loading