diff --git a/CIPPTimers.json b/CIPPTimers.json index ed7cd4c31819..bebf203ef638 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 79336a3eed63..cfb1ca4e888e 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 de8c605da2a0..f8d591457071 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 cbe2d2cbb47c..84f9ad4456b2 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 04440b5926f7..50e56981f25f 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 05ae42c1ed52..0e26f9895474 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 353c6e3fe395..900fb02a9e33 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 6d0cae8a4d61..0783685af1da 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 b36cf8ea3987..60ca9abf1fa0 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 2ffcc5041778..b3e91b0b4bc1 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 58c4446912ba..5b284b29e1f9 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 014e9aed8d7c..35f90390af22 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 e114d1a32977..43ee69370193 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 1a7c6b021b23..038e00dc022b 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 cd8821ae41a2..698fbb42deb3 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 323c6d437219..0ccd44cd0c62 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 19ad42a6c5ad..22719d9a63b4 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 cd1fbce70771..01def39a8b87 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 000000000000..7e87d65243f9 --- /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 587728681c63..eff802655a91 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 85261df84091..b56cb6dc4b79 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 4e2146a003d4..474cfbead706 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 ae62b19cd6eb..62e0fc53a654 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 f8e204371c98..1f0573da72fc 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 2ac7b7ae4898..c65891ba08e6 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 000000000000..88927affbf3e --- /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 44defc8c5b28..a9a9cf745444 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 0d7f1aa20e1d..90257fe4a22d 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 c33d4d006a53..96536f481a0f 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 fc68634b5f29..6639a99ff76f 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 bca90aa87eba..941c5883775e 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 - '
  • ' + "$((Get-Culture).TextInfo.ToTitleCase((convert-skuname -skuname $SkuPartNumber).Tolower()))
  • " + '
  • ' + "$($SkuPartNumber)
  • " } catch {} }) -join '' @@ -1385,13 +1385,8 @@ function Invoke-NinjaOneTenantSync { $LicenseDetails = foreach ($License in $Licenses) { $MatchedSubscriptions = $Subscriptions | Where-Object -Property skuid -EQ $License.skuId - - try { - $FriendlyLicenseName = $((Get-Culture).TextInfo.ToTitleCase((convert-skuname -skuname $License.SkuPartNumber).Tolower())) - } catch { - $FriendlyLicenseName = $License.SkuPartNumber - } - + Write-Information "License info: $($License | ConvertTo-Json -Depth 100)" + $FriendlyLicenseName = $License.skuPartNumber $LicenseUsers = foreach ($SubUser in $Users) { $MatchedLicense = $SubUser.assignedLicenses | Where-Object { $License.skuId -in $_.skuId } diff --git a/host.json b/host.json index e215e52d2205..5462a094374b 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.1.1", + "defaultVersion": "10.1.2", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 23127993ac05..b6132546fce8 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.1.1 +10.1.2