diff --git a/CIPPTimers.json b/CIPPTimers.json index ed7cd4c318197..bebf203ef6382 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -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" diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 index 79336a3eed635..cfb1ca4e888e5 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 @@ -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 } diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index de8c605da2a0b..f8d591457071d 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -34,7 +34,8 @@ function Compare-CIPPIntuneObject { 'agents', 'isSynced' 'locationInfo', - 'templateId' + 'templateId', + 'source' ) $excludeProps = $defaultExcludeProperties + $ExcludeProperties @@ -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)] @@ -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) { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdatePermissionsQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdatePermissionsQueue.ps1 index cbe2d2cbb47cb..84f9ad4456b2a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdatePermissionsQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-UpdatePermissionsQueue.ps1 @@ -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' @@ -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) { @@ -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' } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 index 04440b5926f72..50e56981f25ff 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 @@ -3,7 +3,7 @@ function Invoke-ExecDnsConfig { .FUNCTIONALITY Entrypoint .ROLE - CIPP.AppSettings.ReadWrite + Tenant.Domains.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 index 05ae42c1ed520..0e26f98954748 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 @@ -1,9 +1,9 @@ -Function Invoke-ListContactTemplates { +function Invoke-ListContactTemplates { <# .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - Exchange.Read + Exchange.Contact.Read #> [CmdletBinding()] param($Request, $TriggerMetadata) @@ -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 { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 index 353c6e3fe3959..900fb02a9e332 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxes.ps1 @@ -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 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 index 6d0cae8a4d610..0783685af1da7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 @@ -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 @@ -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) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index b36cf8ea39871..60ca9abf1fa01 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -155,7 +155,7 @@ function Invoke-CIPPOffboardingJob { } } @{ - Condition = { ![string]::IsNullOrEmpty($Options.OnedriveAccess) } + Condition = { $Options.OnedriveAccess.Count -gt 0 } Cmdlet = 'Set-CIPPSharePointPerms' Parameters = @{ tenantFilter = $TenantFilter @@ -166,7 +166,7 @@ function Invoke-CIPPOffboardingJob { } } @{ - Condition = { ![string]::IsNullOrEmpty($Options.AccessNoAutomap) } + Condition = { $Options.AccessNoAutomap.Count -gt 0 } Cmdlet = 'Set-CIPPMailboxAccess' Parameters = @{ tenantFilter = $TenantFilter @@ -179,7 +179,7 @@ function Invoke-CIPPOffboardingJob { } } @{ - Condition = { ![string]::IsNullOrEmpty($Options.AccessAutomap) } + Condition = { $Options.AccessAutomap.Count -gt 0 } Cmdlet = 'Set-CIPPMailboxAccess' Parameters = @{ tenantFilter = $TenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index 2ffcc50417780..b3e91b0b4bc1f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -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 = @{ @@ -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) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 index 58c4446912ba4..5b284b29e1f98 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 @@ -27,15 +27,53 @@ function Invoke-ExecBulkLicense { # Initialize list for bulk license requests $LicenseRequests = [System.Collections.Generic.List[object]]::new() - # Get unique user IDs for this tenant - $UserIds = $TenantRequests.userIds | Select-Object -Unique + # Get unique user IDs for this tenant and normalize to a string array + $UserIds = @( + $TenantRequests | + ForEach-Object { + if ($null -ne $_.userIds) { + @($_.userIds) | ForEach-Object { [string]$_ } + } + } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -Unique + ) + + # Build OData filters in chunks to avoid Graph's OR clause limit + $MaxUserIdFilterClauses = 15 + $UserLookupRequests = [System.Collections.Generic.List[object]]::new() + $AllUsers = [System.Collections.Generic.List[object]]::new() - # Build OData filter for specific users only - $UserIdFilters = $UserIds | ForEach-Object { "id eq '$_'" } - $FilterQuery = $UserIdFilters -join ' or ' + for ($i = 0; $i -lt $UserIds.Count; $i += $MaxUserIdFilterClauses) { + $EndIndex = [Math]::Min($i + $MaxUserIdFilterClauses - 1, $UserIds.Count - 1) + $UserIdChunk = @($UserIds[$i..$EndIndex]) + $UserIdFilters = $UserIdChunk | ForEach-Object { "id eq '$_'" } + $FilterQuery = $UserIdFilters -join ' or ' - # Fetch only the users we need with server-side filtering - $AllUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=$FilterQuery&`$select=id,userPrincipalName,assignedLicenses&top=999" -tenantid $TenantFilter + $UserLookupRequests.Add(@{ + id = "UserLookup$i" + method = 'GET' + url = "/users?`$filter=$FilterQuery&`$select=id,userPrincipalName,assignedLicenses&`$top=999" + }) + } + + # Fetch all user chunks in one Graph bulk request + try { + $UserLookupResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($UserLookupRequests) + } catch { + $LookupError = Get-CippException -Exception $_ + throw "Failed to lookup users before license assignment for tenant $TenantFilter. Error: $($LookupError.NormalizedError)" + } + foreach ($UserLookupResult in $UserLookupResults) { + if ($UserLookupResult.status -lt 200 -or $UserLookupResult.status -gt 299) { + $LookupErrorMessage = $UserLookupResult.body.error.message + if ([string]::IsNullOrEmpty($LookupErrorMessage)) { $LookupErrorMessage = 'Unknown Graph batch error' } + throw "Failed to fetch users for chunk $($UserLookupResult.id): $LookupErrorMessage" + } + foreach ($ChunkUser in @($UserLookupResult.body.value)) { + $AllUsers.Add($ChunkUser) + } + } # Create lookup for quick access $UserLookup = @{} @@ -45,8 +83,17 @@ function Invoke-ExecBulkLicense { # Process each user request foreach ($UserRequest in $TenantRequests) { - $UserId = $UserRequest.userIds + $UserId = @($UserRequest.userIds | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1) + if ($UserId.Count -eq 0) { + $Results.Add("No valid user ID found in request for tenant $TenantFilter") + continue + } + $UserId = $UserId[0] $User = $UserLookup[$UserId] + if ($null -eq $User) { + $Results.Add("User $UserId not found in tenant $TenantFilter") + continue + } $UserPrincipalName = $User.userPrincipalName $LicenseOperation = $UserRequest.LicenseOperation $RemoveAllLicenses = [bool]$UserRequest.RemoveAllLicenses diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 index 014e9aed8d7c7..35f90390af225 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenantOffboardingDefaults.ps1 @@ -14,6 +14,7 @@ function Invoke-EditTenantOffboardingDefaults { # Interact with query parameters or the body of the request. $customerId = $Request.Body.customerId + $defaultDomainName = $Request.Body.defaultDomainName $offboardingDefaults = $Request.Body.offboardingDefaults if (!$customerId) { @@ -47,10 +48,14 @@ function Invoke-EditTenantOffboardingDefaults { $resultText = 'Tenant offboarding defaults updated successfully' } else { # Remove offboarding defaults if empty or null - $Existing = Get-CIPPAzDataTableEntity @PropertiesTable -Filter "PartitionKey eq '$customerId' and RowKey eq 'OffboardingDefaults'" - if ($Existing) { - Remove-AzDataTableEntity @PropertiesTable -Entity $Existing - Write-LogMessage -headers $Headers -tenant $customerId -API $APIName -message "Removed tenant offboarding defaults" -Sev 'Info' + $partitionKeys = @($customerId, $defaultDomainName) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique + $existingDefaults = Get-CIPPAzDataTableEntity @PropertiesTable -Filter "RowKey eq 'OffboardingDefaults'" + $toRemove = $existingDefaults | Where-Object { $_.PartitionKey -in $partitionKeys } + if ($toRemove) { + foreach ($Entity in $toRemove) { + Remove-AzDataTableEntity @PropertiesTable -Entity $Entity + } + Write-LogMessage -headers $Headers -tenant $customerId -API $APIName -message "Removed tenant offboarding defaults for partition keys: $($partitionKeys -join ', ')" -Sev 'Info' } $resultText = 'Tenant offboarding defaults cleared successfully' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index e114d1a329775..43ee69370193e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -89,12 +89,15 @@ function Invoke-ListTenants { # Add offboarding defaults to each tenant foreach ($Tenant in $Tenants) { - $TenantDefaults = $AllOffboardingDefaults | Where-Object { $_.PartitionKey -eq $Tenant.customerId } + $TenantDefaults = $AllOffboardingDefaults | Where-Object { $_.PartitionKey -eq $Tenant.customerId } | Select-Object -First 1 + if (-not $TenantDefaults) { + $TenantDefaults = $AllOffboardingDefaults | Where-Object { $_.PartitionKey -eq $Tenant.initialDomainName } | Select-Object -First 1 + } if ($TenantDefaults) { try { $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value ($TenantDefaults.Value | ConvertFrom-Json) -Force } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Failed to parse offboarding defaults for tenant $($Tenant.customerId): $($_.Exception.Message)" -sev 'Warn' + Write-LogMessage -headers $Headers -API $APIName -message "Failed to parse offboarding defaults for tenant $($Tenant.defaultDomainName): $($_.Exception.Message)" -sev 'Warn' $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value $null -Force } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 index 1a7c6b021b236..038e00dc022b4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 @@ -15,7 +15,10 @@ function Invoke-ExecNamedLocation { $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter $NamedLocationId = $Request.Body.namedLocationId ?? $Request.Query.namedLocationId $Change = $Request.Body.change ?? $Request.Query.change - $Content = $Request.Body.input.value ?? $Request.Query.input.value + $Content = $Request.Body.input ?? $Request.Query.input + #reintroduced because iut can come from autocomplete OR textinput. + if ($content.value) { $content = $content.value } + try { $Results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 index cd8821ae41a20..698fbb42deb3d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-listStandardTemplates.ps1 @@ -1,7 +1,7 @@ function Invoke-listStandardTemplates { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE Tenant.Standards.Read #> diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 index 323c6d4372196..0ccd44cd0c62f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 @@ -43,7 +43,13 @@ function Start-UpdatePermissionsOrchestrator { $Tenants = $Tenants | ForEach-Object { $CPVRow = $CPVRows | Where-Object -Property Tenant -EQ $_.customerId - if (!$CPVRow -or $env:ApplicationID -notin $CPVRow.applicationId -or $SAMPermissions.Timestamp -gt $CPVRow.Timestamp.DateTime -or $CPVRow.Timestamp.DateTime -le (Get-Date).AddDays(-7).ToUniversalTime() -or !$_.defaultDomainName -or ($SAMroles.Timestamp.DateTime -gt $CPVRow.Timestamp.DateTime -and ($SAMRoles.Tenants -contains $_.defaultDomainName -or $SAMRoles.Tenants.value -contains $_.defaultDomainName -or $SAMRoles.Tenants -contains 'AllTenants' -or $SAMRoles.Tenants.value -contains 'AllTenants'))) { + + # Determine retry interval based on last status + # No status or Failed status: retry after 1 day, Success: retry after 7 days + $RetryDays = if (!$CPVRow.LastStatus -or $CPVRow.LastStatus -eq 'Failed') { -1 } else { -7 } + $NeedsRetry = $CPVRow.Timestamp.DateTime -le (Get-Date).AddDays($RetryDays).ToUniversalTime() + + if (!$CPVRow -or $env:ApplicationID -notin $CPVRow.applicationId -or $SAMPermissions.Timestamp -gt $CPVRow.Timestamp.DateTime -or $NeedsRetry -or !$_.defaultDomainName -or ($SAMroles.Timestamp.DateTime -gt $CPVRow.Timestamp.DateTime -and ($SAMRoles.Tenants -contains $_.defaultDomainName -or $SAMRoles.Tenants.value -contains $_.defaultDomainName -or $SAMRoles.Tenants -contains 'AllTenants' -or $SAMRoles.Tenants.value -contains 'AllTenants'))) { $_ } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 index 19ad42a6c5ad1..22719d9a63b4a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 @@ -9,6 +9,19 @@ function Start-LogRetentionCleanup { param() try { + # Check rerun protection - only run once every 24 hours (86400 seconds) + $RerunParams = @{ + TenantFilter = 'AllTenants' + Type = 'LogCleanup' + API = 'LogRetentionCleanup' + Interval = 86400 + } + $Rerun = Test-CIPPRerun @RerunParams + if ($Rerun) { + Write-Host 'Log cleanup was recently executed. Skipping to prevent duplicate execution (runs once every 24 hours)' + return $true + } + # Get retention settings $ConfigTable = Get-CippTable -tablename Config $Filter = "PartitionKey eq 'LogRetention' and RowKey eq 'Settings'" @@ -36,27 +49,50 @@ function Start-LogRetentionCleanup { # Calculate cutoff date $CutoffDate = (Get-Date).AddDays(-$RetentionDays).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - $DeletedCount = 0 + $TotalDeletedCount = 0 + $BatchSize = 5000 # Clean up CIPP Logs if ($PSCmdlet.ShouldProcess('CippLogs', 'Cleaning up old logs')) { $CippLogsTable = Get-CippTable -tablename 'CippLogs' $CutoffFilter = "Timestamp lt datetime'$CutoffDate'" - # Fetch all old log entries - $OldLogs = Get-AzDataTableEntity @CippLogsTable -Filter $CutoffFilter -Property @('PartitionKey', 'RowKey', 'ETag') + # Process deletions in batches of 10k to avoid timeout + $HasMoreRecords = $true + $BatchNumber = 0 + + while ($HasMoreRecords) { + $BatchNumber++ + Write-Host "Processing batch $BatchNumber..." + + # Fetch up to 10k old log entries + $OldLogs = Get-AzDataTableEntity @CippLogsTable -Filter $CutoffFilter -Property @('PartitionKey', 'RowKey') -First $BatchSize + + if ($OldLogs -and ($OldLogs | Measure-Object).Count -gt 0) { + $BatchCount = ($OldLogs | Measure-Object).Count + Remove-AzDataTableEntity @CippLogsTable -Entity $OldLogs -Force + $TotalDeletedCount += $BatchCount + Write-Host "Batch $BatchNumber`: Deleted $BatchCount log entries" + + # If we got less than the batch size, we're done + if ($BatchCount -lt $BatchSize) { + $HasMoreRecords = $false + } + } else { + Write-Host 'No more old logs found' + $HasMoreRecords = $false + } + } - if ($OldLogs) { - Remove-AzDataTableEntity @CippLogsTable -Entity $OldLogs -Force - $DeletedCount = ($OldLogs | Measure-Object).Count - Write-LogMessage -API 'LogRetentionCleanup' -message "Deleted $DeletedCount old log entries (retention: $RetentionDays days)" -Sev 'Info' - Write-Host "Deleted $DeletedCount old log entries" + if ($TotalDeletedCount -gt 0) { + Write-LogMessage -API 'LogRetentionCleanup' -message "Deleted $TotalDeletedCount old log entries in $BatchNumber batch(es) (retention: $RetentionDays days)" -Sev 'Info' + Write-Host "Total deleted: $TotalDeletedCount old log entries" } else { Write-Host 'No old logs found' } } - Write-LogMessage -API 'LogRetentionCleanup' -message "Log cleanup completed. Total logs deleted: $DeletedCount (retention: $RetentionDays days)" -Sev 'Info' + Write-LogMessage -API 'LogRetentionCleanup' -message "Log cleanup completed. Total logs deleted: $TotalDeletedCount (retention: $RetentionDays days)" -Sev 'Info' } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 index cd1fbce70771a..01def39a8b870 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 @@ -81,16 +81,21 @@ function Get-CIPPAzDataTableEntity { foreach ($entity in $finalResults) { if ($entity.SplitOverProps) { - $splitInfoList = $entity.SplitOverProps | ConvertFrom-Json - foreach ($splitInfo in $splitInfoList) { - $mergedData = [string]::Join('', ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ })) - $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force - $propsToRemove = $splitInfo.SplitHeaders - foreach ($prop in $propsToRemove) { - $entity.PSObject.Properties.Remove($prop) + try { + $splitInfoList = $entity.SplitOverProps | ConvertFrom-Json -ErrorAction Stop + foreach ($splitInfo in $splitInfoList) { + $mergedData = [string]::Join('', ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ })) + $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force + $propsToRemove = $splitInfo.SplitHeaders + foreach ($prop in $propsToRemove) { + $entity.PSObject.Properties.Remove($prop) + } } + } catch { + Write-Warning "Failed to process SplitOverProps for entity with PartitionKey='$($entity.PartitionKey)' and RowKey='$($entity.RowKey)': $($_.Exception.Message)" + } finally { + $entity.PSObject.Properties.Remove('SplitOverProps') } - $entity.PSObject.Properties.Remove('SplitOverProps') } } diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxesReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxesReport.ps1 new file mode 100644 index 0000000000000..7e87d65243f99 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxesReport.ps1 @@ -0,0 +1,74 @@ +function Get-CIPPMailboxesReport { + <# + .SYNOPSIS + Generates a mailboxes report from the CIPP Reporting database + + .DESCRIPTION + Retrieves mailbox data for a tenant from the reporting database + + .PARAMETER TenantFilter + The tenant to generate the report for + + .EXAMPLE + Get-CIPPMailboxesReport -TenantFilter 'contoso.onmicrosoft.com' + Gets all mailboxes for the tenant from the report database + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have mailbox data + $AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes' + $Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPMailboxesReport -TenantFilter $Tenant + foreach ($Result in $TenantResults) { + # Add Tenant property to each result + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'MailboxesReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + # Get mailboxes from reporting DB + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + if (-not $MailboxItems) { + throw 'No mailbox data found in reporting database. Sync the report data first.' + } + + # Get the most recent cache timestamp + $CacheTimestamp = ($MailboxItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse mailbox data + $AllMailboxes = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $MailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' }) { + $Mailbox = $Item.Data | ConvertFrom-Json + + # Add cache timestamp + $Mailbox | Add-Member -NotePropertyName 'CacheTimestamp' -NotePropertyValue $CacheTimestamp -Force + + $AllMailboxes.Add($Mailbox) + } + + return $AllMailboxes | Sort-Object -Property displayName + + } catch { + Write-LogMessage -API 'MailboxesReport' -tenant $TenantFilter -message "Failed to generate mailboxes report: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 b/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 index 587728681c63a..eff802655a91c 100644 --- a/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 @@ -17,8 +17,8 @@ function Set-CIPPUserLicense { $LicenseRequests.Add([PSCustomObject]@{ UserId = $UserId UserPrincipalName = $UserPrincipalName - AddLicenses = $AddLicenses - RemoveLicenses = $RemoveLicenses + AddLicenses = @($AddLicenses) + RemoveLicenses = @($RemoveLicenses) IsReplace = $false }) } @@ -31,6 +31,23 @@ function Set-CIPPUserLicense { if ($UserSettings) { $DefaultUsageLocation = (ConvertFrom-Json $UserSettings.JSON -Depth 5 -ErrorAction SilentlyContinue).usageLocation.value } $DefaultUsageLocation ??= 'US' + # Normalize license arrays to avoid sending null skuIds to Graph + foreach ($Request in $LicenseRequests) { + $Request.AddLicenses = @( + @($Request.AddLicenses) | + ForEach-Object { [string]$_ } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + $Request.RemoveLicenses = @( + @($Request.RemoveLicenses) | + ForEach-Object { [string]$_ } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + if ([string]::IsNullOrWhiteSpace($Request.UserPrincipalName)) { + $Request.UserPrincipalName = $Request.UserId + } + } + # Process Replace operations first (remove all licenses) $ReplaceRequests = $LicenseRequests | Where-Object { $_.IsReplace -and $_.RemoveLicenses.Count -gt 0 } if ($ReplaceRequests.Count -gt 0) { @@ -41,7 +58,7 @@ function Set-CIPPUserLicense { url = "/users/$($Request.UserId)/assignLicense" body = @{ 'addLicenses' = @() - 'removeLicenses' = @($Request.RemoveLicenses) + 'removeLicenses' = $Request.RemoveLicenses } headers = @{ 'Content-Type' = 'application/json' } } @@ -72,7 +89,7 @@ function Set-CIPPUserLicense { url = "/users/$($Request.UserId)/assignLicense" body = @{ 'addLicenses' = @($AddLicensesArray) - 'removeLicenses' = $Request.IsReplace ? @() : @($Request.RemoveLicenses) + 'removeLicenses' = $Request.IsReplace ? @() : $Request.RemoveLicenses } headers = @{ 'Content-Type' = 'application/json' } } @@ -133,7 +150,7 @@ function Set-CIPPUserLicense { url = "/users/$($Request.UserId)/assignLicense" body = @{ 'addLicenses' = @($AddLicensesArray) - 'removeLicenses' = $Request.IsReplace ? @() : @($Request.RemoveLicenses) + 'removeLicenses' = $Request.IsReplace ? @() : $Request.RemoveLicenses } headers = @{ 'Content-Type' = 'application/json' } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 85261df840917..b56cb6dc4b796 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -99,6 +99,13 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $Filter = "PartitionKey eq 'CATemplate' and RowKey eq '$($Settings.TemplateList.value)'" $Policy = (Get-CippAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json -Depth 10 + # Override the template's state with the Drift Standard's state if specified + # This ensures drift detection compares against the desired state, not the original template state + if ($Settings.state -and $Settings.state -ne 'donotchange') { + Write-Information "Overriding template state from '$($Policy.state)' to '$($Settings.state)' for drift comparison" + $Policy.state = $Settings.state + } + $CheckExististing = $AllCAPolicies | Where-Object -Property displayName -EQ $Settings.TemplateList.label if (!$CheckExististing) { if ($Policy.conditions.userRiskLevels.Count -gt 0 -or $Policy.conditions.signInRiskLevels.Count -gt 0) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 4e2146a003d4d..474cfbead7064 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableBasicAuthSMTP' try { - $CurrentInfo = New-CippDbRequest -TenantFilter $Tenant -Type 'Get-TransportConfig' + $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' $SMTPusers = New-CippDbRequest -TenantFilter $Tenant -Type 'CASMailbox' | Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index ae62b19cd6eb2..62e0fc53a654f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -215,7 +215,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { } # Add to alert objects list efficiently foreach ($Mailbox in $MailboxesToUpdate) { - $AlertObjects.Add($Mailbox) + $AlertObjects.Add($Mailbox.UserPrincipalName) } } @@ -232,7 +232,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { } # Add to alert objects list efficiently foreach ($Mailbox in $MailboxesWithPlanIssues) { - $AlertObjects.Add($Mailbox) + $AlertObjects.Add($Mailbox.UserPrincipalName) } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 index f8e204371c98c..1f0573da72fc2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 @@ -52,10 +52,12 @@ function Invoke-CIPPStandardRetentionPolicyTag { return } + $CurrentAgeLimitForRetention = ([timespan]$CurrentState.AgeLimitForRetention).TotalDays + $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and ($CurrentState.RetentionEnabled -eq $true) -and ($CurrentState.RetentionAction -eq 'PermanentlyDelete') -and - ($CurrentState.AgeLimitForRetention -eq ([timespan]::FromDays($Settings.AgeLimitForRetention))) -and + ($CurrentAgeLimitForRetention -eq $Settings.AgeLimitForRetention) -and ($CurrentState.Type -eq 'DeletedItems') -and ($PolicyState.RetentionPolicyTagLinks -contains $PolicyName) @@ -125,7 +127,7 @@ function Invoke-CIPPStandardRetentionPolicyTag { $CurrentValue = @{ retentionEnabled = $CurrentState.RetentionEnabled retentionAction = $CurrentState.RetentionAction - ageLimitForRetention = $CurrentState.AgeLimitForRetention.TotalDays + ageLimitForRetention = $CurrentAgeLimitForRetention type = $CurrentState.Type policyTagLinked = $PolicyState.RetentionPolicyTagLinks -contains $PolicyName diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index 2ac7b7ae48989..c65891ba08e6d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsFederationConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1','Teams_Room_Standard') + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsFederationConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { return $true @@ -41,9 +41,8 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTenantFederationConfiguration' -CmdParams @{Identity = 'Global' } | - Select-Object * - } - catch { + Select-Object * + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsFederationConfiguration state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -56,15 +55,18 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { 'AllowAllExternal' { $AllowFederatedUsers = $true $AllowedDomains = $AllowAllKnownDomains + $AllowedDomainsAsAList = @() $BlockedDomains = @() } 'BlockAllExternal' { $AllowFederatedUsers = $false $AllowedDomains = $AllowAllKnownDomains + $AllowedDomainsAsAList = @() $BlockedDomains = @() } 'AllowSpecificExternal' { $AllowFederatedUsers = $true + $AllowedDomains = $null $BlockedDomains = @() if ($null -ne $Settings.DomainList) { $AllowedDomainsAsAList = @($Settings.DomainList).Split(',').Trim() @@ -74,7 +76,8 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { } 'BlockSpecificExternal' { $AllowFederatedUsers = $true - $AllowedDomainsAsAList = 'AllowAllKnownDomains' + $AllowedDomains = $AllowAllKnownDomains + $AllowedDomainsAsAList = @() if ($null -ne $Settings.DomainList) { $BlockedDomains = @($Settings.DomainList).Split(',').Trim() } else { @@ -87,17 +90,86 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { } } + # Parse current state based on DomainControl mode $CurrentAllowedDomains = $CurrentState.AllowedDomains - if ($CurrentAllowedDomains.GetType().Name -eq 'PSObject') { - $CurrentAllowedDomains = $CurrentAllowedDomains.Domain | Sort-Object - $DomainList = ($CurrentAllowedDomains | Sort-Object) ?? @() - $AllowedDomainsMatches = -not (Compare-Object -ReferenceObject $AllowedDomainsAsAList -DifferenceObject $DomainList) - } elseif ($CurrentAllowedDomains.GetType().Name -eq 'Deserialized.Microsoft.Rtc.Management.WritableConfig.Settings.Edge.AllowAllKnownDomains') { - $CurrentAllowedDomains = $CurrentAllowedDomains.ToString() - $AllowedDomainsMatches = $CurrentAllowedDomains -eq $AllowedDomains.ToString() + $CurrentBlockedDomains = $CurrentState.BlockedDomains + $IsCurrentAllowAllKnownDomains = $false + $AllowedDomainsMatches = $false + $BlockedDomainsMatches = $false + + # Check if current allowed domains is AllowAllKnownDomains, and parse specific domains if not + if ($CurrentAllowedDomains) { + if ($CurrentAllowedDomains.GetType().Name -eq 'PSObject') { + $properties = Get-Member -InputObject $CurrentAllowedDomains -MemberType Properties, NoteProperty + if (($null -ne $CurrentAllowedDomains.AllowAllKnownDomains) -or + (Get-Member -InputObject $CurrentAllowedDomains -Name 'AllowAllKnownDomains') -or + (!$properties -or $properties.Count -eq 0)) { + $IsCurrentAllowAllKnownDomains = $true + Write-Information "Current AllowedDomains is AllowAllKnownDomains" + } else { + # Parse specific allowed domains list + if ($null -ne $CurrentAllowedDomains.AllowedDomain -or (Get-Member -InputObject $CurrentAllowedDomains -Name 'AllowedDomain')) { + $CurrentAllowedDomains = @($CurrentAllowedDomains.AllowedDomain | ForEach-Object { $_.Domain }) | Sort-Object + Write-Information "Current AllowedDomains (extracted): $($CurrentAllowedDomains -join ', ')" + } elseif ($null -ne $CurrentAllowedDomains.Domain -or (Get-Member -InputObject $CurrentAllowedDomains -Name 'Domain')) { + $CurrentAllowedDomains = @($CurrentAllowedDomains.Domain) | Sort-Object + Write-Information "Current AllowedDomains (via Domain property): $($CurrentAllowedDomains -join ', ')" + } else { + $CurrentAllowedDomains = @() + } + } + } elseif ($CurrentAllowedDomains.GetType().Name -eq 'Deserialized.Microsoft.Rtc.Management.WritableConfig.Settings.Edge.AllowAllKnownDomains') { + $IsCurrentAllowAllKnownDomains = $true + Write-Information "Current AllowedDomains is AllowAllKnownDomains (Deserialized type)" + } + } else { + $CurrentAllowedDomains = @() + } + + # Parse blocked domains upfront (always extract Domain property if present) + if ($CurrentBlockedDomains -is [System.Collections.IEnumerable] -and $CurrentBlockedDomains -isnot [string]) { + $blockedDomainsArray = @($CurrentBlockedDomains) + if ($blockedDomainsArray.Count -gt 0) { + $firstElement = $blockedDomainsArray[0] + $hasDomainProperty = ($null -ne $firstElement.Domain) -or (Get-Member -InputObject $firstElement -Name 'Domain' -MemberType Properties, NoteProperty) + + if ($hasDomainProperty) { + $CurrentBlockedDomains = @($blockedDomainsArray | ForEach-Object { $_.Domain }) | Sort-Object + Write-Information "Current BlockedDomains (extracted): $($CurrentBlockedDomains -join ', ')" + } else { + $CurrentBlockedDomains = @($blockedDomainsArray) | Sort-Object + Write-Information "Current BlockedDomains (plain strings): $($CurrentBlockedDomains -join ', ')" + } + } else { + $CurrentBlockedDomains = @() + } + } else { + $CurrentBlockedDomains = @() + } + + # Mode-specific validation + switch ($DomainControl) { + 'AllowAllExternal' { + $AllowedDomainsMatches = $IsCurrentAllowAllKnownDomains + $BlockedDomainsMatches = (!$CurrentBlockedDomains -or @($CurrentBlockedDomains).Count -eq 0) + } + 'BlockAllExternal' { + # When blocking all, federation must be disabled + $AllowedDomainsMatches = $true + $BlockedDomainsMatches = $true + } + 'AllowSpecificExternal' { + $AllowedDomainsMatches = -not (Compare-Object -ReferenceObject $AllowedDomainsAsAList -DifferenceObject $CurrentAllowedDomains) + $BlockedDomainsMatches = (!$CurrentBlockedDomains -or @($CurrentBlockedDomains).Count -eq 0) + } + 'BlockSpecificExternal' { + # Allowed should be AllowAllKnownDomains, blocked domains already parsed above + $AllowedDomainsMatches = $IsCurrentAllowAllKnownDomains + $BlockedDomainsMatches = -not (Compare-Object -ReferenceObject $BlockedDomains -DifferenceObject $CurrentBlockedDomains) + } } - $BlockedDomainsMatches = -not (Compare-Object -ReferenceObject $BlockedDomains -DifferenceObject $CurrentState.BlockedDomains) + $ExpectedBlockedDomains = $BlockedDomains ?? @() $StateIsCorrect = ($CurrentState.AllowTeamsConsumer -eq $Settings.AllowTeamsConsumer) -and ($CurrentState.AllowFederatedUsers -eq $AllowFederatedUsers) -and @@ -115,10 +187,10 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { BlockedDomains = $BlockedDomains } - if (!$AllowedDomainsAsAList) { - $cmdParams.AllowedDomains = $AllowedDomains - } else { + if ($AllowedDomainsAsAList -and $AllowedDomainsAsAList.Count -gt 0) { $cmdParams.AllowedDomainsAsAList = $AllowedDomainsAsAList + } else { + $cmdParams.AllowedDomains = $AllowedDomains } try { @@ -143,17 +215,47 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'FederationConfiguration' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + $CurrentAllowedDomainsForReport = if ($IsCurrentAllowAllKnownDomains) { + 'AllowAllKnownDomains' + } elseif ($CurrentAllowedDomains) { + $CurrentAllowedDomains + } else { + @() + } + + # Normalize expected allowed domains for reporting + $ExpectedAllowedDomainsForReport = if ($AllowedDomainsAsAList -and $AllowedDomainsAsAList.Count -gt 0) { + $AllowedDomainsAsAList + } elseif ($AllowedDomains) { + 'AllowAllKnownDomains' + } else { + @() + } + + # Normalize blocked domains for reporting + $CurrentBlockedDomainsForReport = if ($null -ne $CurrentBlockedDomains -and @($CurrentBlockedDomains).Count -gt 0) { + @($CurrentBlockedDomains) + } else { + @() + } + + $ExpectedBlockedDomainsForReport = if ($null -ne $ExpectedBlockedDomains -and @($ExpectedBlockedDomains).Count -gt 0) { + @($ExpectedBlockedDomains) + } else { + @() + } + $CurrentValue = @{ AllowTeamsConsumer = $CurrentState.AllowTeamsConsumer AllowFederatedUsers = $CurrentState.AllowFederatedUsers - AllowedDomains = if ($CurrentAllowedDomains.GetType().Name -eq 'Deserialized.Microsoft.Rtc.Management.WritableConfig.Settings.Edge.AllowAllKnownDomains') { $CurrentAllowedDomains.ToString() } else { $CurrentAllowedDomains } - BlockedDomains = $CurrentState.BlockedDomains + AllowedDomains = $CurrentAllowedDomainsForReport + BlockedDomains = $CurrentBlockedDomainsForReport } $ExpectedValue = @{ AllowTeamsConsumer = $Settings.AllowTeamsConsumer AllowFederatedUsers = $AllowFederatedUsers - AllowedDomains = $AllowedDomains - BlockedDomains = $BlockedDomains + AllowedDomains = $ExpectedAllowedDomainsForReport + BlockedDomains = $ExpectedBlockedDomainsForReport } Set-CIPPStandardsCompareField -FieldName 'standards.TeamsFederationConfiguration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 new file mode 100644 index 0000000000000..88927affbf3ee --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardWindowsBackupRestore.ps1 @@ -0,0 +1,101 @@ +function Invoke-CIPPStandardWindowsBackupRestore { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) WindowsBackupRestore + .SYNOPSIS + (Label) Set Windows Backup and Restore state + .DESCRIPTION + (Helptext) Configures the Windows Backup and Restore enrollment setting in Intune. When enabled, users see a restore page during Windows Autopilot/OOBE that allows them to restore their apps and settings from a previous device backup. **Before you can restore a backup, a policy to enable it on devices must be set up in Settings Catalog.** + (DocsDescription) Configures the Windows Backup and Restore (WBfO) device enrollment setting in Intune. This feature allows users to restore apps and settings from a previous device backup during Windows setup. Enabling this shows a restore page during enrollment (OOBE) so users can migrate their workspace configuration to a new device. More information can be found in [Microsoft's documentation.](https://learn.microsoft.com/en-us/intune/intune-service/enrollment/windows-backup-restore) + .NOTES + CAT + Intune Standards + TAG + EXECUTIVETEXT + Controls the Windows Backup and Restore for Organizations feature in Intune. When enabled, employees setting up new devices can restore their apps and settings from a previous backup during Windows enrollment. This streamlines device provisioning, reduces setup time for new or replacement devices, and improves the employee experience during device transitions. + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.WindowsBackupRestore.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"},{"label":"Not Configured","value":"notConfigured"}]} + IMPACT + Low Impact + ADDEDDATE + 2026-02-26 + POWERSHELLEQUIVALENT + Graph API + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + [CmdletBinding()] + param($Tenant, $Settings) + + $TestResult = Test-CIPPStandardLicense -StandardName 'WindowsBackupRestore' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + + if ($TestResult -eq $false) { + return $true + } + + # Get state value using null-coalescing operator + $WantedState = $Settings.state.value ?? $Settings.state + + try { + $Config = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?$filter=deviceEnrollmentConfigurationType eq ''windowsRestore''' -tenantid $Tenant + $CurrentState = $Config.state + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve Windows Backup and Restore configuration. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return + } + + $StateIsCorrect = $CurrentState -eq $WantedState + + $CurrentValue = [PSCustomObject]@{ + state = $CurrentState + } + $ExpectedValue = [PSCustomObject]@{ + state = $WantedState + } + + # Input validation + if ([string]::IsNullOrWhiteSpace($WantedState)) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'WindowsBackupRestore: Invalid state parameter set' -sev Error + return + } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Windows Backup and Restore is already set to $WantedState." -sev Info + } else { + try { + $Body = @{ + '@odata.type' = '#microsoft.graph.windowsRestoreDeviceEnrollmentConfiguration' + state = $WantedState + } | ConvertTo-Json -Depth 10 + + New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($Config.id)" -tenantid $Tenant -type PATCH -body $Body + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set Windows Backup and Restore to $WantedState." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Windows Backup and Restore to $WantedState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Windows Backup and Restore is set correctly to $WantedState." -sev Info + } else { + Write-StandardsAlert -message "Windows Backup and Restore is not set correctly. Expected: $WantedState, Current: $CurrentState" -object @{ CurrentState = $CurrentState; WantedState = $WantedState } -tenant $Tenant -standardName 'WindowsBackupRestore' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Windows Backup and Restore is not set correctly to $WantedState." -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.WindowsBackupRestore' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'WindowsBackupRestore' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 index 44defc8c5b28f..a9a9cf7454441 100644 --- a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 @@ -53,7 +53,10 @@ function Test-CIPPGDAPRelationships { 'M365 GDAP SharePoint Administrator', 'M365 GDAP Authentication Policy Administrator', 'M365 GDAP Privileged Role Administrator', - 'M365 GDAP Privileged Authentication Administrator' + 'M365 GDAP Privileged Authentication Administrator', + 'M365 GDAP Billing Administrator', + 'M365 GDAP Global Reader', + 'M365 GDAP Domain Name Administrator' ) $RoleAssignableGroups = $SAMUserMemberships | Where-Object { $_.isAssignableToRole } $NestedGroups = [System.Collections.Generic.List[object]]::new() @@ -85,10 +88,10 @@ function Test-CIPPGDAPRelationships { }) | Out-Null } } - if ($CIPPGroupCount -lt 12) { + if ($CIPPGroupCount -lt 15) { $GDAPissues.add([PSCustomObject]@{ Type = 'Warning' - Issue = "We only found $($CIPPGroupCount) of the 12 required groups. If you have migrated outside of CIPP this is to be expected. Please perform an access check to make sure you have the correct set of permissions." + Issue = "We only found $($CIPPGroupCount) of the 15 required groups. If you have migrated outside of CIPP this is to be expected. Please perform an access check to make sure you have the correct set of permissions." Tenant = '*Partner Tenant' Relationship = 'None' Link = 'https://docs.cipp.app/setup/gdap/troubleshooting#groups' diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index 0d7f1aa20e1d5..90257fe4a22d2 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -6,6 +6,7 @@ function Invoke-CippWebhookProcessing { $Resource, $Operations, $CIPPURL, + $AlertComment, $APIName = 'Process webhook', $Headers ) @@ -79,7 +80,7 @@ function Invoke-CippWebhookProcessing { # Save audit log entry to table $LocationInfo = $Data.CIPPLocationInfo | ConvertFrom-Json -ErrorAction SilentlyContinue $AuditRecord = $Data.AuditRecord | ConvertFrom-Json -ErrorAction SilentlyContinue - $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -AlertComment $WebhookRule.AlertComment + $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -AlertComment $AlertComment $JsonContent = @{ Title = $GenerateJSON.Title ActionUrl = $GenerateJSON.ButtonUrl @@ -89,6 +90,7 @@ function Invoke-CippWebhookProcessing { PotentialLocationInfo = $LocationInfo ActionsTaken = $ActionResults AuditRecord = $AuditRecord + AlertComment = $AlertComment } | ConvertTo-Json -Depth 15 -Compress $CIPPAlert = @{ @@ -102,7 +104,7 @@ function Invoke-CippWebhookProcessing { $LogId = Send-CIPPAlert @CIPPAlert $AuditLogLink = '{0}/tenant/administration/audit-logs/log?logId={1}&tenantFilter={2}' -f $CIPPURL, $LogId, $Tenant.defaultDomainName - $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink -AlertComment $WebhookRule.AlertComment + $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink -AlertComment $AlertComment Write-Host 'Going to create the content' foreach ($action in $ActionList ) { diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index c33d4d006a538..96536f481a0fd 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -149,11 +149,12 @@ function Test-CIPPAuditLogRules { # Check if the TenantFilter matches any tenant in the expanded list or AllTenants if ($ExpandedTenants.value -contains $TenantFilter -or $ExpandedTenants.value -contains 'AllTenants') { [pscustomobject]@{ - Tenants = $Tenants - Excluded = ($ConfigEntry.excludedTenants | ConvertFrom-Json -ErrorAction SilentlyContinue) - Conditions = $ConfigEntry.Conditions - Actions = $ConfigEntry.Actions - LogType = $ConfigEntry.Type + Tenants = $Tenants + Excluded = ($ConfigEntry.excludedTenants | ConvertFrom-Json -ErrorAction SilentlyContinue) + Conditions = $ConfigEntry.Conditions + Actions = $ConfigEntry.Actions + LogType = $ConfigEntry.Type + AlertComment = $ConfigEntry.AlertComment } } } @@ -553,6 +554,7 @@ function Test-CIPPAuditLogRules { clause = $finalCondition expectedAction = $actions CIPPClause = $CIPPClause + AlertComment = $Config.AlertComment } } } catch { @@ -574,6 +576,7 @@ function Test-CIPPAuditLogRules { $ReturnedData = foreach ($item in $ReturnedData) { $item.CIPPAction = $clause.expectedAction $item.CIPPClause = $clause.CIPPClause -join ' and ' + $item.CIPPAlertComment = $clause.AlertComment $MatchedRules.Add($clause.CIPPClause -join ' and ') $item } @@ -601,6 +604,7 @@ function Test-CIPPAuditLogRules { Data = $AuditLog CIPPURL = [string]$CIPPURL TenantFilter = $TenantFilter + AlertComment = $AuditLog.CIPPAlertComment } try { Invoke-CippWebhookProcessing @Webhook diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 index fc68634b5f295..6639a99ff76f4 100644 --- a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -246,7 +246,7 @@ function Invoke-HuduExtensionSync { $post = '' - $licenseOut = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { Convert-SKUname -skuName $_.SkuPartNumber -ConvertTable $LicTable } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } + $licenseOut = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { $_.SkuPartNumber } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } $licenseHTML = $licenseOut | ConvertTo-Html -PreContent $pre -PostContent $post -Fragment | Out-String } @@ -519,11 +519,10 @@ function Invoke-HuduExtensionSync { $userLicenses = ($user.AssignedLicenses.SkuID | ForEach-Object { $UserLic = $_ $SkuPartNumber = ($Licenses | Where-Object { $_.SkuId -eq $UserLic }).SkuPartNumber - $DisplayName = Convert-SKUname -skuName $SkuPartNumber -ConvertTable $LicTable - if (!$DisplayName) { - $DisplayName = $SkuPartNumber + if (!$SkuPartNumber) { + $SkuPartNumber = 'Unknown License' } - $DisplayName + $SkuPartNumber }) -join ', ' $UserOneDriveDetails = $OneDriveDetails | Where-Object { $_.ownerPrincipalName -eq $user.userPrincipalName } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index bca90aa87eba0..941c5883775e4 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -358,7 +358,7 @@ function Invoke-NinjaOneTenantSync { # Get the license overview for the tenant if ($Licenses) { - $LicensesParsed = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { (Get-Culture).TextInfo.ToTitleCase((convert-skuname -skuname $_.SkuPartNumber).Tolower()) } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } + $LicensesParsed = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { $_.skuPartNumber } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } } Write-Verbose "$(Get-Date) - Parsing Device Compliance Policies" @@ -891,7 +891,7 @@ function Invoke-NinjaOneTenantSync { $UserLic = $_ try { $SkuPartNumber = ($Licenses | Where-Object { $_.SkuId -eq $UserLic }).SkuPartNumber - '