diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index a67ab955d4bb..866cb12d44fe 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -102,6 +102,7 @@ function Add-CIPPAzDataTableEntity { $propertiesToRemove = [System.Collections.Generic.List[object]]::new() foreach ($key in $SingleEnt.Keys) { + if ($key -in @('RowKey', 'PartitionKey')) { continue } $newEntitySize = [System.Text.Encoding]::UTF8.GetByteCount($($newEntity | ConvertTo-Json -Compress)) if ($newEntitySize -lt $MaxRowSize) { $propertySize = [System.Text.Encoding]::UTF8.GetByteCount($SingleEnt[$key].ToString()) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 new file mode 100644 index 000000000000..703bb86e6ba1 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 @@ -0,0 +1,32 @@ +function Get-CIPPAlertSmtpAuthSuccess { + <# + .FUNCTIONALITY + Entrypoint – Check sign-in logs for SMTP AUTH with success status + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + # Graph API endpoint for sign-ins + $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=clientAppUsed eq 'SMTP' and status/errorCode eq 0" + + # Call Graph API for the given tenant + $SignIns = New-GraphGetRequest -uri $uri -tenantid $TenantFilter + + # Select only the properties you care about + $AlertData = $SignIns.value | Select-Object userPrincipalName, createdDateTime, clientAppUsed, ipAddress, status + + # Write results into the alert pipeline + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + + } catch { + # Suppress errors if no data returned + # Uncomment if you want explicit error logging + # Write-AlertMessage -tenant $($TenantFilter) -message "Failed to query SMTP AUTH sign-ins for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index 5473c27351ca..469ad123c416 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -364,7 +364,7 @@ function Push-ExecOnboardTenantQueue { $Table = Get-CippTable -tablename 'templates' $ExistingTemplates = Get-CippazDataTableEntity @Table -Filter "PartitionKey eq 'StandardsTemplateV2'" | Where-Object { $_.JSON -match 'AllTenants' } foreach ($AllTenantsTemplate in $ExistingTemplates) { - $object = $AllTenantesTemplate.JSON | ConvertFrom-Json + $object = $AllTenantsTemplate.JSON | ConvertFrom-Json $NewExcludedTenants = [system.collections.generic.list[object]]::new() if (!$object.excludedTenants) { $object | Add-Member -MemberType NoteProperty -Name 'excludedTenants' -Value @() -Force diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 index d400e7dc6437..ef4eb8819a99 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 @@ -1,121 +1,127 @@ using namespace System.Net Function Invoke-ExecExtensionMapping { - <# + <# .FUNCTIONALITY Entrypoint .ROLE CIPP.Extension.ReadWrite #> - [CmdletBinding()] - param($Request, $TriggerMetadata) + [CmdletBinding()] + param($Request, $TriggerMetadata) - $APIName = $Request.Params.CIPPEndpoint - $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' - $Table = Get-CIPPTable -TableName CippMapping + $Table = Get-CIPPTable -TableName CippMapping - if ($Request.Query.List) { - switch ($Request.Query.List) { - 'HaloPSA' { - $Result = Get-HaloMapping -CIPPMapping $Table - } - 'NinjaOne' { - $Result = Get-NinjaOneOrgMapping -CIPPMapping $Table - } - 'NinjaOneFields' { - $Result = Get-NinjaOneFieldMapping -CIPPMapping $Table - } - 'Hudu' { - $Result = Get-HuduMapping -CIPPMapping $Table - } - 'HuduFields' { - $Result = Get-HuduFieldMapping -CIPPMapping $Table - } - 'Sherweb' { - $Result = Get-SherwebMapping -CIPPMapping $Table - } - 'HaloPSAFields' { - $TicketTypes = Get-HaloTicketType - $Result = @{'TicketTypes' = $TicketTypes } - } - 'PWPushFields' { - $Accounts = Get-PwPushAccount - $Result = @{ - 'Accounts' = $Accounts - } - } + if ($Request.Query.List) { + switch ($Request.Query.List) { + 'HaloPSA' { + $Result = Get-HaloMapping -CIPPMapping $Table + } + 'NinjaOne' { + $Result = Get-NinjaOneOrgMapping -CIPPMapping $Table + } + 'NinjaOneFields' { + $Result = Get-NinjaOneFieldMapping -CIPPMapping $Table + } + 'Hudu' { + $Result = Get-HuduMapping -CIPPMapping $Table + } + 'HuduFields' { + $Result = Get-HuduFieldMapping -CIPPMapping $Table + } + 'Sherweb' { + $Result = Get-SherwebMapping -CIPPMapping $Table + } + 'HaloPSAFields' { + $TicketTypes = Get-HaloTicketType + $Outcomes = Get-HaloTicketOutcome + $Result = @{ + 'TicketTypes' = $TicketTypes + 'Outcomes' = $Outcomes } + } + 'PWPushFields' { + $Accounts = Get-PwPushAccount + $Result = @{ + 'Accounts' = $Accounts + } + } } + } - try { - if ($Request.Query.AddMapping) { - switch ($Request.Query.AddMapping) { - 'Sherweb' { - $Result = Set-SherwebMapping -CIPPMapping $Table -APIName $APIName -Request $Request - } - 'HaloPSA' { - $Result = Set-HaloMapping -CIPPMapping $Table -APIName $APIName -Request $Request - } - 'NinjaOne' { - $Result = Set-NinjaOneOrgMapping -CIPPMapping $Table -APIName $APIName -Request $Request - Register-CIPPExtensionScheduledTasks - } - 'NinjaOneFields' { - $Result = Set-NinjaOneFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -TriggerMetadata $TriggerMetadata - Register-CIPPExtensionScheduledTasks - } - 'Hudu' { - $Result = Set-HuduMapping -CIPPMapping $Table -APIName $APIName -Request $Request - Register-CIPPExtensionScheduledTasks - } - 'HuduFields' { - $Result = Set-ExtensionFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -Extension 'Hudu' - Register-CIPPExtensionScheduledTasks - } - } + try { + if ($Request.Query.AddMapping) { + switch ($Request.Query.AddMapping) { + 'Sherweb' { + $Result = Set-SherwebMapping -CIPPMapping $Table -APIName $APIName -Request $Request + } + 'HaloPSA' { + $Result = Set-HaloMapping -CIPPMapping $Table -APIName $APIName -Request $Request + } + 'NinjaOne' { + $Result = Set-NinjaOneOrgMapping -CIPPMapping $Table -APIName $APIName -Request $Request + Register-CIPPExtensionScheduledTasks + } + 'NinjaOneFields' { + $Result = Set-NinjaOneFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -TriggerMetadata $TriggerMetadata + Register-CIPPExtensionScheduledTasks } - $StatusCode = [HttpStatusCode]::OK - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = "Mapping API failed. $($ErrorMessage.NormalizedError)" - Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage - $StatusCode = [HttpStatusCode]::InternalServerError + 'Hudu' { + $Result = Set-HuduMapping -CIPPMapping $Table -APIName $APIName -Request $Request + Register-CIPPExtensionScheduledTasks + } + 'HuduFields' { + $Result = Set-ExtensionFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -Extension 'Hudu' + Register-CIPPExtensionScheduledTasks + } + } } + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Mapping API failed. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } - try { - if ($Request.Query.AutoMapping) { - switch ($Request.Query.AutoMapping) { - 'NinjaOne' { - $Batch = [PSCustomObject]@{ - 'NinjaAction' = 'StartAutoMapping' - 'FunctionName' = 'NinjaOneQueue' - } - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'NinjaOneOrchestrator' - Batch = @($Batch) - } - #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Host "Started permissions orchestration with ID = '$InstanceId'" - $Result = 'AutoMapping Request has been queued. Exact name matches will appear first and matches on device names and serials will take longer. Please check the CIPP Logbook and refresh the page once complete.' - } - - } + try { + if ($Request.Query.AutoMapping) { + switch ($Request.Query.AutoMapping) { + 'NinjaOne' { + $Batch = [PSCustomObject]@{ + 'NinjaAction' = 'StartAutoMapping' + 'FunctionName' = 'NinjaOneQueue' + } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'NinjaOneOrchestrator' + Batch = @($Batch) + } + #Write-Host ($InputObject | ConvertTo-Json) + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Host "Started permissions orchestration with ID = '$InstanceId'" + $Result = 'AutoMapping Request has been queued. Exact name matches will appear first and matches on device names and serials will take longer. Please check the CIPP Logbook and refresh the page once complete.' } - $StatusCode = [HttpStatusCode]::OK - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Result = "Mapping API failed. $($ErrorMessage.NormalizedError)" - Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage - $StatusCode = [HttpStatusCode]::InternalServerError + + } } + $StatusCode = [HttpStatusCode]::OK + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Mapping API failed. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } - # Associate values to output bindings by calling 'Push-OutputBinding'. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = $Result - }) + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Result + }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 index 957b0acb0dad..b4110caf036e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecModifyCalPerms { +function Invoke-ExecModifyCalPerms { <# .FUNCTIONALITY Entrypoint @@ -64,6 +64,7 @@ Function Invoke-ExecModifyCalPerms { $Modification = $Permission.Modification $CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false $FolderName = $Permission.FolderName ?? 'Calendar' + $SendNotificationToUser = $Permission.SendNotificationToUser ?? $false Write-LogMessage -headers $Headers -API $APIName -message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug' @@ -76,16 +77,17 @@ Function Invoke-ExecModifyCalPerms { try { Write-LogMessage -headers $Headers -API $APIName -message "Processing target user: $TargetUser" -Sev 'Debug' $Params = @{ - APIName = $APIName - Headers = $Headers - RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null } - TenantFilter = $TenantFilter - UserID = $UserId - folderName = $FolderName - UserToGetPermissions = $TargetUser - LoggingName = $TargetUser - Permissions = $PermissionLevel - CanViewPrivateItems = $CanViewPrivateItems + APIName = $APIName + Headers = $Headers + RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null } + TenantFilter = $TenantFilter + UserID = $UserId + folderName = $FolderName + UserToGetPermissions = $TargetUser + LoggingName = $TargetUser + Permissions = $PermissionLevel + CanViewPrivateItems = $CanViewPrivateItems + SendNotificationToUser = $SendNotificationToUser } # Write-Host "Request params: $($Params | ConvertTo-Json)" 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 b22280ba01b3..c0a732a3f6b8 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 @@ -17,7 +17,7 @@ Function Invoke-ListMailboxes { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter try { - $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' + $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 cmdlet = 'Get-Mailbox' @@ -76,7 +76,8 @@ Function Invoke-ListMailboxes { @{ Name = 'LicensedForLitigationHold'; Expression = { ($_.PersistedCapabilities -contains 'BPOS_S_DlpAddOn' -or $_.PersistedCapabilities -contains 'BPOS_S_Enterprise') } }, ComplianceTagHoldApplied, RetentionHoldEnabled, - InPlaceHolds + InPlaceHolds, + RetentionPolicy # This select also exists in ListUserMailboxDetails and should be updated if this is changed here diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecManageRetentionPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecManageRetentionPolicies.ps1 new file mode 100644 index 000000000000..51f2c7503ae8 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecManageRetentionPolicies.ps1 @@ -0,0 +1,257 @@ +using namespace System.Net + +Function Invoke-ExecManageRetentionPolicies { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.RetentionPolicies.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + $Results = [System.Collections.Generic.List[string]]::new() + $TenantFilter = $Request.Query.tenantFilter ?? $Request.body.tenantFilter + $CmdletArray = [System.Collections.ArrayList]::new() + $CmdletMetadataArray = [System.Collections.ArrayList]::new() + $GuidToMetadataMap = @{} + + if ([string]::IsNullOrEmpty($TenantFilter)) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = "Tenant filter is required" + }) + return + } + + try { + # Helper function to add cmdlet to bulk array + function Add-BulkCmdlet { + param($CmdletName, $Parameters, $ExpectedResult, $Operation, $Identity = "") + + $OperationGuid = [Guid]::NewGuid().ToString() + + $CmdletObj = @{ + CmdletInput = @{ + CmdletName = $CmdletName + Parameters = $Parameters + } + OperationGuid = $OperationGuid + } + + $CmdletMetadata = [PSCustomObject]@{ + ExpectedResult = $ExpectedResult + Operation = $Operation + Identity = $Identity + OperationGuid = $OperationGuid + } + + $null = $CmdletArray.Add($CmdletObj) + $null = $CmdletMetadataArray.Add($CmdletMetadata) + $GuidToMetadataMap[$OperationGuid] = $CmdletMetadata + } + + # Create Retention Policies + $CreatePolicies = $Request.body.CreatePolicies + if ($CreatePolicies) { + foreach ($Policy in $CreatePolicies) { + if ([string]::IsNullOrEmpty($Policy.Name)) { + $Results.Add("Failed to create policy - Name is required") + continue + } + + $cmdParams = @{ + Name = $Policy.Name + } + + if ($Policy.RetentionPolicyTagLinks) { + $cmdParams.RetentionPolicyTagLinks = $Policy.RetentionPolicyTagLinks + } + + Add-BulkCmdlet -CmdletName 'New-RetentionPolicy' -Parameters $cmdParams -ExpectedResult "Successfully created retention policy: $($Policy.Name)" -Operation 'Create' -Identity $Policy.Name + } + } + + # Modify Retention Policies + $ModifyPolicies = $Request.body.ModifyPolicies + if ($ModifyPolicies) { + foreach ($Policy in $ModifyPolicies) { + if ([string]::IsNullOrEmpty($Policy.Identity)) { + $Results.Add("Failed to modify policy - Identity is required") + continue + } + + $cmdParams = @{ + Identity = $Policy.Identity + } + + if ($Policy.Name) { + $cmdParams.Name = $Policy.Name + } + + # Handle tag modifications - need to get current policy first for add/remove operations + if ($Policy.AddTags -or $Policy.RemoveTags) { + try { + $currentPolicy = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionPolicy' -cmdParams @{Identity = $Policy.Identity} + $currentTags = $currentPolicy.RetentionPolicyTagLinks + } catch { + $Results.Add("Failed to modify policy $($Policy.Identity) - Could not retrieve current policy") + continue + } + + if ($Policy.AddTags) { + $newTagsList = [System.Collections.ArrayList]::new() + if ($currentTags) { + foreach ($tag in $currentTags) { $null = $newTagsList.Add($tag) } + } + foreach ($tag in $Policy.AddTags) { + if ($tag -notin $newTagsList) { $null = $newTagsList.Add($tag) } + } + $cmdParams.RetentionPolicyTagLinks = @($newTagsList) + } + + if ($Policy.RemoveTags) { + $newTagsList = [System.Collections.ArrayList]::new() + if ($currentTags) { + foreach ($tag in $currentTags) { + if ($tag -notin $Policy.RemoveTags) { $null = $newTagsList.Add($tag) } + } + } + $cmdParams.RetentionPolicyTagLinks = @($newTagsList) + } + } elseif ($Policy.RetentionPolicyTagLinks) { + $cmdParams.RetentionPolicyTagLinks = $Policy.RetentionPolicyTagLinks + } + + Add-BulkCmdlet -CmdletName 'Set-RetentionPolicy' -Parameters $cmdParams -ExpectedResult "Successfully modified retention policy: $($Policy.Identity)" -Operation 'Modify' -Identity $Policy.Identity + } + } + + # Delete Retention Policies + $DeletePolicies = $Request.body.DeletePolicies + if ($DeletePolicies) { + foreach ($PolicyIdentity in $DeletePolicies) { + if ([string]::IsNullOrEmpty($PolicyIdentity)) { + $Results.Add("Failed to delete policy - Identity is required") + continue + } + + # Check if policy is assigned to mailboxes (do this before bulk processing) + $assignedMailboxes = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{ + Filter = "RetentionPolicy -eq '$PolicyIdentity'" + ResultSize = 1 + } -ErrorAction SilentlyContinue + + if ($assignedMailboxes) { + $Results.Add("Cannot delete retention policy $PolicyIdentity - still assigned to mailboxes") + continue + } + + Add-BulkCmdlet -CmdletName 'Remove-RetentionPolicy' -Parameters @{Identity = $PolicyIdentity; Confirm = $false} -ExpectedResult "Successfully deleted retention policy: $PolicyIdentity" -Operation 'Delete' -Identity $PolicyIdentity + } + } + + # Execute bulk operations + if ($CmdletArray.Count -gt 0) { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Executing $($CmdletArray.Count) retention policy operations" -Sev 'Info' -tenant $TenantFilter + + if ($CmdletArray.Count -gt 1) { + # Use bulk processing + $BulkResults = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($CmdletArray) -ReturnWithCommand $true + + # Process bulk results using GUID mapping + if ($BulkResults -is [hashtable] -and $BulkResults.Keys.Count -gt 0) { + foreach ($cmdletName in $BulkResults.Keys) { + foreach ($result in $BulkResults[$cmdletName]) { + $operationGuid = $result.OperationGuid + + if ($operationGuid -and $GuidToMetadataMap.ContainsKey($operationGuid)) { + $metadata = $GuidToMetadataMap[$operationGuid] + + if ($result.error) { + $ErrorMessage = try { (Get-CippException -Exception $result.error).NormalizedError } catch { $result.error } + $Message = "Failed to $($metadata.Operation.ToLower()) retention policy $($metadata.Identity): $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + } else { + Write-LogMessage -headers $Request.Headers -API $APINAME -message $metadata.ExpectedResult -Sev 'Info' -tenant $TenantFilter + $Results.Add($metadata.ExpectedResult) + } + } + } + } + } + } else { + # Single operation + $CmdletObj = $CmdletArray[0] + $CmdletMetadata = $CmdletMetadataArray[0] + + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $CmdletObj.CmdletInput.CmdletName -cmdParams $CmdletObj.CmdletInput.Parameters + Write-LogMessage -headers $Request.Headers -API $APINAME -message $CmdletMetadata.ExpectedResult -Sev 'Info' -tenant $TenantFilter + $Results.Add($CmdletMetadata.ExpectedResult) + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to $($CmdletMetadata.Operation.ToLower()) retention policy $($CmdletMetadata.Identity): $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + } + } + } + + $StatusCode = [HttpStatusCode]::OK + + # Simple response logic + if ($CreatePolicies -or $ModifyPolicies -or $DeletePolicies) { + # For any operations, return the results messages + $GraphRequest = @($Results) + } else { + # For listing, return all policies or specific policy - wrapped in try-catch + try { + $SpecificName = $Request.Query.name + if ($SpecificName) { + # Get specific policy by name + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionPolicy' -cmdParams @{Identity = $SpecificName} + } else { + # Get all policies + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionPolicy' + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = if ($Request.Query.name) { + "Failed to retrieve retention policy '$($Request.Query.name)': $ErrorMessage" + } else { + "Failed to retrieve retention policies: $ErrorMessage" + } + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = @($Results) + } + } + + # If no results are found, we will return an empty message to prevent null reference errors in the frontend + $GraphRequest = $GraphRequest ?? @() + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to manage retention policies: $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = @($Results) + } + + # If no results are found, we will return an empty message to prevent null reference errors in the frontend + $GraphRequest = $GraphRequest ?? @() + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $GraphRequest + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecManageRetentionTags.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecManageRetentionTags.ps1 new file mode 100644 index 000000000000..4d78667bd9cb --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecManageRetentionTags.ps1 @@ -0,0 +1,358 @@ +using namespace System.Net + +Function Invoke-ExecManageRetentionTags { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.RetentionPolicies.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + $Results = [System.Collections.Generic.List[string]]::new() + $TenantFilter = $Request.Query.tenantFilter ?? $Request.body.tenantFilter + $CmdletArray = [System.Collections.ArrayList]::new() + $CmdletMetadataArray = [System.Collections.ArrayList]::new() + $GuidToMetadataMap = @{} + + if ([string]::IsNullOrEmpty($TenantFilter)) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = "Tenant filter is required" + }) + return + } + + try { + # Helper function to add cmdlet to bulk array + function Add-BulkCmdlet { + param($CmdletName, $Parameters, $ExpectedResult, $Operation, $Identity = "") + + $OperationGuid = [Guid]::NewGuid().ToString() + + $CmdletObj = @{ + CmdletInput = @{ + CmdletName = $CmdletName + Parameters = $Parameters + } + OperationGuid = $OperationGuid + } + + $CmdletMetadata = [PSCustomObject]@{ + ExpectedResult = $ExpectedResult + Operation = $Operation + Identity = $Identity + OperationGuid = $OperationGuid + } + + $null = $CmdletArray.Add($CmdletObj) + $null = $CmdletMetadataArray.Add($CmdletMetadata) + $GuidToMetadataMap[$OperationGuid] = $CmdletMetadata + } + + # Validation function for retention tag parameters + function Test-RetentionTagParams { + param($Tag, $IsModification = $false) + + if (-not $IsModification) { + if ([string]::IsNullOrEmpty($Tag.Name)) { + return "Tag Name is required" + } + + if ([string]::IsNullOrEmpty($Tag.Type)) { + return "Tag Type is required" + } + + # Valid tag types + $validTypes = @('All', 'Inbox', 'SentItems', 'DeletedItems', 'Drafts', 'Outbox', 'JunkEmail', 'Journal', 'SyncIssues', 'ConversationHistory', 'Personal', 'RecoverableItems', 'NonIpmRoot', 'LegacyArchiveJournals', 'Clutter', 'Calendar', 'Notes', 'Tasks', 'Contacts', 'RssSubscriptions', 'ManagedCustomFolder') + if ($Tag.Type -notin $validTypes) { + return "Invalid Type '$($Tag.Type)'. Valid types: $($validTypes -join ', ')" + } + + # Validate RetentionAction compatibility with Type (only for creation) + if ($Tag.RetentionAction) { + switch ($Tag.RetentionAction) { + 'MoveToArchive' { + $allowedTypesForArchive = @('All', 'Personal', 'RecoverableItems') + if ($Tag.Type -notin $allowedTypesForArchive) { + return "RetentionAction 'MoveToArchive' can only be used with tag types: $($allowedTypesForArchive -join ', '). Current type: '$($Tag.Type)'" + } + } + 'DeleteAndAllowRecovery' { + $excludedTypesForDelete = @('RecoverableItems') + if ($Tag.Type -in $excludedTypesForDelete) { + return "RetentionAction 'DeleteAndAllowRecovery' cannot be used with tag type '$($Tag.Type)'" + } + } + 'PermanentlyDelete' { + $excludedTypesForPermanentDelete = @('RecoverableItems') + if ($Tag.Type -in $excludedTypesForPermanentDelete) { + return "RetentionAction 'PermanentlyDelete' cannot be used with tag type '$($Tag.Type)'" + } + } + } + } + + # Validate RetentionEnabled and RetentionAction relationship (only for creation) + if ($Tag.RetentionEnabled -eq $true -and [string]::IsNullOrEmpty($Tag.RetentionAction)) { + return "RetentionAction is required when RetentionEnabled is set to true" + } + } + + # Common validations for both create and modify + if ($Tag.Name) { + if ($Tag.Name -match '[\\/:*?\"<>|]') { + return "Tag name contains invalid characters. Avoid using: \ / : * ? `" < > |" + } + + if ($Tag.Name.Length -gt 64) { + return "Tag name cannot exceed 64 characters" + } + } + + if ($Tag.RetentionAction) { + $validActions = @('DeleteAndAllowRecovery', 'PermanentlyDelete', 'MoveToArchive', 'MarkAsPastRetentionLimit') + if ($Tag.RetentionAction -notin $validActions) { + return "Invalid RetentionAction '$($Tag.RetentionAction)'. Valid actions: $($validActions -join ', ')" + } + } + + if ($Tag.AgeLimitForRetention -and ($Tag.AgeLimitForRetention -lt 0 -or $Tag.AgeLimitForRetention -gt 24855)) { + return "AgeLimitForRetention must be between 0 and 24855 days" + } + + return $null + } + + # Create Retention Tags + $CreateTags = $Request.body.CreateTags + if ($CreateTags) { + foreach ($Tag in $CreateTags) { + $validationError = Test-RetentionTagParams -Tag $Tag -IsModification $false + if ($validationError) { + $Results.Add("Failed to create tag '$($Tag.Name)': $validationError") + continue + } + + $cmdParams = @{ + Name = $Tag.Name + Type = $Tag.Type + } + + if ($Tag.AgeLimitForRetention) { + $cmdParams.AgeLimitForRetention = $Tag.AgeLimitForRetention + } + + if ($Tag.RetentionAction) { + $cmdParams.RetentionAction = $Tag.RetentionAction + } + + if ($Tag.Comment) { + $cmdParams.Comment = $Tag.Comment + } + + if ($Tag.RetentionEnabled -ne $null) { + $cmdParams.RetentionEnabled = $Tag.RetentionEnabled + } + + if ($Tag.LocalizedComment) { + $cmdParams.LocalizedComment = $Tag.LocalizedComment + } + + if ($Tag.LocalizedRetentionPolicyTagName) { + $cmdParams.LocalizedRetentionPolicyTagName = $Tag.LocalizedRetentionPolicyTagName + } + + $resultParts = [System.Collections.ArrayList]::new() + $null = $resultParts.Add("Successfully created retention tag: $($Tag.Name) (Type: $($Tag.Type)") + if ($Tag.RetentionAction) { $null = $resultParts.Add(", Action: $($Tag.RetentionAction)") } + if ($Tag.AgeLimitForRetention) { $null = $resultParts.Add(", Age: $($Tag.AgeLimitForRetention) days") } + $null = $resultParts.Add(")") + $expectedResult = $resultParts -join "" + + Add-BulkCmdlet -CmdletName 'New-RetentionPolicyTag' -Parameters $cmdParams -ExpectedResult $expectedResult -Operation 'Create' -Identity $Tag.Name + } + } + + # Modify Retention Tags + $ModifyTags = $Request.body.ModifyTags + if ($ModifyTags) { + foreach ($Tag in $ModifyTags) { + if ([string]::IsNullOrEmpty($Tag.Identity)) { + $Results.Add("Failed to modify tag - Identity is required") + continue + } + + # Use basic validation for modifications + $validationError = Test-RetentionTagParams -Tag $Tag -IsModification $true + if ($validationError) { + $Results.Add("Failed to modify tag '$($Tag.Identity)': $validationError") + continue + } + + $cmdParams = @{ + Identity = $Tag.Identity + } + + if ($Tag.Name) { + $cmdParams.Name = $Tag.Name + } + + if ($Tag.AgeLimitForRetention) { + $cmdParams.AgeLimitForRetention = $Tag.AgeLimitForRetention + } + + if ($Tag.RetentionAction) { + $cmdParams.RetentionAction = $Tag.RetentionAction + } + + if ($Tag.Comment) { + $cmdParams.Comment = $Tag.Comment + } + + if ($Tag.RetentionEnabled -ne $null) { + $cmdParams.RetentionEnabled = $Tag.RetentionEnabled + } + + if ($Tag.LocalizedComment) { + $cmdParams.LocalizedComment = $Tag.LocalizedComment + } + + if ($Tag.LocalizedRetentionPolicyTagName) { + $cmdParams.LocalizedRetentionPolicyTagName = $Tag.LocalizedRetentionPolicyTagName + } + + Add-BulkCmdlet -CmdletName 'Set-RetentionPolicyTag' -Parameters $cmdParams -ExpectedResult "Successfully modified retention tag: $($Tag.Identity)" -Operation 'Modify' -Identity $Tag.Identity + } + } + + # Delete Retention Tags + $DeleteTags = $Request.body.DeleteTags + if ($DeleteTags) { + foreach ($TagIdentity in $DeleteTags) { + if ([string]::IsNullOrEmpty($TagIdentity)) { + $Results.Add("Failed to delete tag - Identity is required") + continue + } + + # Check if tag is used in any retention policies + $AllPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionPolicy' -ErrorAction SilentlyContinue + $policiesUsingTag = $AllPolicies | Where-Object { + $_.RetentionPolicyTagLinks -contains $TagIdentity + } + + if ($policiesUsingTag) { + $policyNames = ($policiesUsingTag | ForEach-Object { $_.Name }) -join ', ' + $Results.Add("Cannot delete retention tag '$TagIdentity' - still used in policies: $policyNames") + continue + } + + Add-BulkCmdlet -CmdletName 'Remove-RetentionPolicyTag' -Parameters @{Identity = $TagIdentity; Confirm = $false} -ExpectedResult "Successfully deleted retention tag: $TagIdentity" -Operation 'Delete' -Identity $TagIdentity + } + } + + # Execute bulk operations + if ($CmdletArray.Count -gt 0) { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Executing $($CmdletArray.Count) retention tag operations" -Sev 'Info' -tenant $TenantFilter + + if ($CmdletArray.Count -gt 1) { + # Use bulk processing + $BulkResults = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($CmdletArray) -ReturnWithCommand $true + + # Process bulk results using GUID mapping + if ($BulkResults -is [hashtable] -and $BulkResults.Keys.Count -gt 0) { + foreach ($cmdletName in $BulkResults.Keys) { + foreach ($result in $BulkResults[$cmdletName]) { + $operationGuid = $result.OperationGuid + + if ($operationGuid -and $GuidToMetadataMap.ContainsKey($operationGuid)) { + $metadata = $GuidToMetadataMap[$operationGuid] + + if ($result.error) { + $ErrorMessage = try { (Get-CippException -Exception $result.error).NormalizedError } catch { $result.error } + $Message = "Failed to $($metadata.Operation.ToLower()) retention tag $($metadata.Identity): $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + } else { + Write-LogMessage -headers $Request.Headers -API $APINAME -message $metadata.ExpectedResult -Sev 'Info' -tenant $TenantFilter + $Results.Add($metadata.ExpectedResult) + } + } + } + } + } + } else { + # Single operation + $CmdletObj = $CmdletArray[0] + $CmdletMetadata = $CmdletMetadataArray[0] + + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $CmdletObj.CmdletInput.CmdletName -cmdParams $CmdletObj.CmdletInput.Parameters + Write-LogMessage -headers $Request.Headers -API $APINAME -message $CmdletMetadata.ExpectedResult -Sev 'Info' -tenant $TenantFilter + $Results.Add($CmdletMetadata.ExpectedResult) + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to $($CmdletMetadata.Operation.ToLower()) retention tag $($CmdletMetadata.Identity): $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + } + } + } + + $StatusCode = [HttpStatusCode]::OK + + # Simple response logic + if ($CreateTags -or $ModifyTags -or $DeleteTags) { + # For any operations, return the results messages + $GraphRequest = @($Results) + } else { + # For listing, return all tags or specific tag - wrapped in try-catch + try { + $SpecificName = $Request.Query.name + if ($SpecificName) { + # Get specific tag by name + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionPolicyTag' -cmdParams @{Identity = $SpecificName} + } else { + # Get all tags + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RetentionPolicyTag' + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = if ($Request.Query.name) { + "Failed to retrieve retention tag '$($Request.Query.name)': $ErrorMessage" + } else { + "Failed to retrieve retention tags: $ErrorMessage" + } + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = @($Results) + } + } + + # If no results are found, we will return an empty message to prevent null reference errors in the frontend + $GraphRequest = $GraphRequest ?? @() + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to manage retention tags: $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = @($Results) + } + + # If no results are found, we will return an empty message to prevent null reference errors in the frontend + $GraphRequest = $GraphRequest ?? @() + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $GraphRequest + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecSetMailboxRetentionPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecSetMailboxRetentionPolicies.ps1 new file mode 100644 index 000000000000..8188ea0b1f30 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Mailbox Retention/Invoke-ExecSetMailboxRetentionPolicies.ps1 @@ -0,0 +1,151 @@ +using namespace System.Net + +Function Invoke-ExecSetMailboxRetentionPolicies { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + $Results = [System.Collections.Generic.List[string]]::new() + $TenantFilter = $Request.Query.tenantFilter ?? $Request.body.tenantFilter + $CmdletArray = [System.Collections.ArrayList]::new() + $CmdletMetadataArray = [System.Collections.ArrayList]::new() + $GuidToMetadataMap = @{} + + if ([string]::IsNullOrEmpty($TenantFilter)) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = "Tenant filter is required" + }) + return + } + + try { + $PolicyName = $Request.body.PolicyName + $Mailboxes = $Request.body.Mailboxes + + # Validate required parameters + if ([string]::IsNullOrEmpty($PolicyName)) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = "PolicyName is required" + }) + return + } + + if (-not $Mailboxes -or $Mailboxes.Count -eq 0) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = "Mailboxes array is required" + }) + return + } + + # Helper function to add cmdlet to bulk array + function Add-BulkCmdlet { + param($CmdletName, $Parameters, $MailboxIdentity) + + $OperationGuid = [Guid]::NewGuid().ToString() + + $CmdletObj = @{ + CmdletInput = @{ + CmdletName = $CmdletName + Parameters = $Parameters + } + OperationGuid = $OperationGuid + } + + $CmdletMetadata = [PSCustomObject]@{ + MailboxIdentity = $MailboxIdentity + OperationGuid = $OperationGuid + } + + $null = $CmdletArray.Add($CmdletObj) + $null = $CmdletMetadataArray.Add($CmdletMetadata) + $GuidToMetadataMap[$OperationGuid] = $CmdletMetadata + } + + # Process each mailbox + foreach ($MailboxIdentity in $Mailboxes) { + if ([string]::IsNullOrEmpty($MailboxIdentity)) { + $Results.Add("Failed to apply retention policy to empty mailbox identity") + continue + } + + Add-BulkCmdlet -CmdletName 'Set-Mailbox' -Parameters @{Identity = $MailboxIdentity; RetentionPolicy = $PolicyName} -MailboxIdentity $MailboxIdentity + } + + # Execute bulk operations + if ($CmdletArray.Count -gt 0) { + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Applying retention policy '$PolicyName' to $($CmdletArray.Count) mailboxes" -Sev 'Info' -tenant $TenantFilter + + if ($CmdletArray.Count -gt 1) { + # Use bulk processing + $BulkResults = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($CmdletArray) -ReturnWithCommand $true + + # Process bulk results using GUID mapping + if ($BulkResults -is [hashtable] -and $BulkResults.Keys.Count -gt 0) { + foreach ($cmdletName in $BulkResults.Keys) { + foreach ($result in $BulkResults[$cmdletName]) { + $operationGuid = $result.OperationGuid + + if ($operationGuid -and $GuidToMetadataMap.ContainsKey($operationGuid)) { + $metadata = $GuidToMetadataMap[$operationGuid] + + if ($result.error) { + $ErrorMessage = try { (Get-CippException -Exception $result.error).NormalizedError } catch { $result.error } + $Message = "Failed to apply retention policy '$PolicyName' to $($metadata.MailboxIdentity): $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + } else { + $Message = "Successfully applied retention policy '$PolicyName' to $($metadata.MailboxIdentity)" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Info' -tenant $TenantFilter + $Results.Add($Message) + } + } + } + } + } + } else { + # Single operation + $CmdletObj = $CmdletArray[0] + $CmdletMetadata = $CmdletMetadataArray[0] + + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $CmdletObj.CmdletInput.CmdletName -cmdParams $CmdletObj.CmdletInput.Parameters + $Message = "Successfully applied retention policy '$PolicyName' to $($CmdletMetadata.MailboxIdentity)" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Info' -tenant $TenantFilter + $Results.Add($Message) + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to apply retention policy '$PolicyName' to $($CmdletMetadata.MailboxIdentity): $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + } + } + } + + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $Message = "Failed to set mailbox retention policies: $ErrorMessage" + Write-LogMessage -headers $Request.Headers -API $APINAME -message $Message -Sev 'Error' -tenant $TenantFilter + $Results.Add($Message) + $StatusCode = [HttpStatusCode]::Forbidden + } + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = @($Results) } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemovePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemovePolicy.ps1 index a0941f3854e6..8830342f07d9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemovePolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemovePolicy.ps1 @@ -18,13 +18,13 @@ function Invoke-RemovePolicy { $TenantFilter = $Request.Query.tenantFilter ?? $Request.body.tenantFilter $PolicyId = $Request.Query.ID ?? $Request.body.ID $UrlName = $Request.Query.URLName ?? $Request.body.URLName - + $BaseEndpoint = $UrlName -eq 'managedAppPolicies' ? 'deviceAppManagement' : 'deviceManagement' if (!$PolicyId) { exit } + try { + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/$($BaseEndpoint)/$($UrlName)('$($PolicyId)')" -type DELETE -tenant $TenantFilter - # $unAssignRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($PolicyId)')/assign" -type POST -Body '{"assignments":[]}' -tenant $TenantFilter - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($UrlName)('$($PolicyId)')" -type DELETE -tenant $TenantFilter - $Results = "Successfully deleted the policy with ID: $($PolicyId)" + $Results = "Successfully deleted the $UrlName policy with ID: $($PolicyId)" Write-LogMessage -headers $Headers -API $APINAME -message $Results -Sev Info -tenant $TenantFilter $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 index 970d02136dfd..9ba69968dee8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 @@ -11,6 +11,7 @@ function Invoke-ExecOffboardUser { param($Request, $TriggerMetadata) $AllUsers = $Request.Body.user.value $TenantFilter = $request.Body.tenantFilter.value ? $request.Body.tenantFilter.value : $request.Body.tenantFilter + $OffboardingOptions = $Request.Body | Select-Object * -ExcludeProperty user, tenantFilter, Scheduled $Results = foreach ($username in $AllUsers) { try { $APIName = 'ExecOffboardUser' @@ -27,7 +28,7 @@ function Invoke-ExecOffboardUser { Parameters = [pscustomobject]@{ Username = $Username APIName = 'Scheduled Offboarding' - options = $Request.Body + options = $OffboardingOptions RunScheduled = $true } ScheduledTime = $Request.Body.Scheduled.date @@ -39,7 +40,7 @@ function Invoke-ExecOffboardUser { } Add-CIPPScheduledTask -Task $taskObject -hidden $false -Headers $Headers } else { - Invoke-CIPPOffboardingJob -Username $Username -TenantFilter $TenantFilter -Options $Request.Body -APIName $APIName -Headers $Headers + Invoke-CIPPOffboardingJob -Username $Username -TenantFilter $TenantFilter -Options $OffboardingOptions -APIName $APIName -Headers $Headers } $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 index 34f7a1d7c5b1..b47ec0338113 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 @@ -270,6 +270,7 @@ function Invoke-ListUserMailboxDetails { AutoExpandingArchive = $AutoExpandingArchiveEnabled RecipientTypeDetails = $MailboxDetailedRequest.RecipientTypeDetails Mailbox = $MailboxDetailedRequest + RetentionPolicy = $MailboxDetailedRequest.RetentionPolicy MailboxActionsData = ($MailboxDetailedRequest | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDriftClone.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDriftClone.ps1 new file mode 100644 index 000000000000..a3f803adc864 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDriftClone.ps1 @@ -0,0 +1,47 @@ +using namespace System.Net + +function Invoke-ExecDriftClone { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Standards.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + try { + $TemplateId = $Request.Body.id + + if (-not $TemplateId) { + $Results = [pscustomobject]@{ + 'Results' = 'Template ID is required' + 'Success' = $false + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = $Results + }) + return + } + $CloneResult = New-CippStandardsDriftClone -TemplateId $TemplateId -UpgradeToDrift -Headers $Request.Headers + $Results = [pscustomobject]@{ + 'Results' = $CloneResult + 'Success' = $true + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results + }) + } catch { + $Results = [pscustomobject]@{ + 'Results' = "Failed to create drift clone: $($_.Exception.Message)" + 'Success' = $false + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = $Results + }) + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index ab8383486b3d..b887e41d3366 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -8,7 +8,7 @@ function Start-UserTasksOrchestrator { $Table = Get-CippTable -tablename 'ScheduledTasks' $1HourAgo = (Get-Date).AddHours(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - $Filter = "TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Running' and Timestamp lt datetime'$1HourAgo')" + $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Running' and Timestamp lt datetime'$1HourAgo'))" $tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter $Batch = [System.Collections.Generic.List[object]]::new() $TenantList = Get-Tenants -IncludeErrors diff --git a/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 b/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 index 768bced2eca9..8d55790e0f54 100644 --- a/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 +++ b/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 @@ -27,7 +27,10 @@ function Test-CIPPStandardLicense { [string]$TenantFilter, [Parameter(Mandatory = $true)] - [string[]]$RequiredCapabilities + [string[]]$RequiredCapabilities, + + [Parameter(Mandatory = $false)] + [switch]$SkipLog ) try { @@ -41,16 +44,20 @@ function Test-CIPPStandardLicense { } if ($Capabilities.Count -le 0) { - Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Tenant does not have the required capability to run standard $StandardName`: The tenant needs one of the following service plans: $($RequiredCapabilities -join ',')" -sev Error - Set-CIPPStandardsCompareField -FieldName "standards.$StandardName" -FieldValue "License Missing: This tenant is not licensed for the following capabilities: $($RequiredCapabilities -join ',')" -Tenant $TenantFilter - Write-Host "Tenant does not have the required capability to run standard $StandardName - $($RequiredCapabilities -join ','). Exiting" + if (!$SkipLog.IsPresent) { + Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Tenant does not have the required capability to run standard $StandardName`: The tenant needs one of the following service plans: $($RequiredCapabilities -join ',')" -sev Error + Set-CIPPStandardsCompareField -FieldName "standards.$StandardName" -FieldValue "License Missing: This tenant is not licensed for the following capabilities: $($RequiredCapabilities -join ',')" -Tenant $TenantFilter + Write-Host "Tenant does not have the required capability to run standard $StandardName - $($RequiredCapabilities -join ','). Exiting" + } return $false } Write-Host "Tenant has the required capabilities for standard $StandardName" return $true } catch { - Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Error checking license capabilities for standard $StandardName`: $($_.Exception.Message)" -sev Error - Set-CIPPStandardsCompareField -FieldName "standards.$StandardName" -FieldValue "License Missing: Error checking license capabilities - $($_.Exception.Message)" -Tenant $TenantFilter + if (!$SkipLog.IsPresent) { + Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Error checking license capabilities for standard $StandardName`: $($_.Exception.Message)" -sev Error + Set-CIPPStandardsCompareField -FieldName "standards.$StandardName" -FieldValue "License Missing: Error checking license capabilities - $($_.Exception.Message)" -Tenant $TenantFilter + } return $false } } diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 88feab4169e2..0ec869f6e596 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -197,12 +197,6 @@ function New-CIPPCAPolicy { $JSONObj.conditions.users.$groupType = @(Replace-GroupNameWithId -groupNames $JSONObj.conditions.users.$groupType) } } - - if ($JSONObj.conditions.users.includeUsers.Count -eq 0) { - Write-Information 'No users matched in this policy, setting to none' - $JSONObj.conditions.users.includeUsers = 'none' - } - } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to replace displayNames for conditional access rule $($JSONObj.displayName). Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage diff --git a/Modules/CIPPCore/Public/New-CippStandardsDriftClone.ps1 b/Modules/CIPPCore/Public/New-CippStandardsDriftClone.ps1 new file mode 100644 index 000000000000..463ff41b20a2 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CippStandardsDriftClone.ps1 @@ -0,0 +1,40 @@ +function New-CippStandardsDriftClone { + [CmdletBinding()] + param ( + [Parameter(Mandatory)][string]$TemplateId, + [Parameter(Mandatory)][switch]$UpgradeToDrift, + $Headers + ) + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'StandardsTemplateV2' and RowKey eq '$TemplateId'" + $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $data = $Entity.JSON | ConvertFrom-Json + $data.excludedTenants = @() #blank excluded Tenants + $data.tenantFilter = @(@{ value = 'Copied Standard'; label = 'Copied Standard' }) + $data.GUID = [guid]::NewGuid().ToString() + $data.templateName = "$($data.templateName) (Drift Clone)" + if ($UpgradeToDrift) { + try { + $data | Add-Member -MemberType NoteProperty -Name 'type' -Value 'drift' -Force + if ($null -ne $data.standards) { + foreach ($prop in $data.standards.PSObject.Properties) { + $actions = $prop.Value.action + if ($actions -and $actions.Count -gt 0) { + if ($actions | Where-Object { $_.value -eq 'remediate' }) { + $prop.Value | Add-Member -MemberType NoteProperty -Name 'autoRemediate' -Value $true -Force + } + $prop.Value.action = @(@{ 'label' = 'Report'; 'value' = 'Report' }) + } + } + } + $Entity.JSON = "$(ConvertTo-Json -InputObject $data -Compress -Depth 100)" + $Entity.RowKey = "$($data.GUID)" + $Entity.GUID = $data.GUID + $update = Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + return 'Clone Completed successfully' + } catch { + return "Failed to Clone template to Drift Template: $_" + } + } +} diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index e1d60e66bb87..2c972715f672 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -22,8 +22,9 @@ function Send-CIPPAlert { Write-Information 'Trying to send email' try { if ($Config.email -like '*@*' -or $altEmail -like '*@*') { - $Recipients = if ($AltEmail) { $AltEmail } - else { + $Recipients = if ($AltEmail) { + [pscustomobject]@{EmailAddress = @{Address = $AltEmail } } + } else { $Config.email.split($(if ($Config.email -like '*,*') { ',' } else { ';' })).trim() | ForEach-Object { if ($_ -like '*@*') { [pscustomobject]@{EmailAddress = @{Address = $_ } } } } } $PowerShellBody = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 b/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 index 72abad183130..57d2c4695680 100644 --- a/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 @@ -6,15 +6,15 @@ function Set-CIPPCalendarPermission { $RemoveAccess, $TenantFilter, $UserID, - $folderName, + $FolderName, $UserToGetPermissions, $LoggingName, $Permissions, - [bool]$CanViewPrivateItems + [bool]$CanViewPrivateItems, + [bool]$SendNotificationToUser = $false ) try { - # If a pretty logging name is not provided, use the ID instead if ([string]::IsNullOrWhiteSpace($LoggingName) -and $RemoveAccess) { $LoggingName = $RemoveAccess @@ -23,33 +23,37 @@ function Set-CIPPCalendarPermission { } $CalParam = [PSCustomObject]@{ - Identity = "$($UserID):\$folderName" - AccessRights = @($Permissions) - User = $UserToGetPermissions + Identity = "$($UserID):\$FolderName" + AccessRights = @($Permissions) + User = $UserToGetPermissions + SendNotificationToUser = $SendNotificationToUser } if ($CanViewPrivateItems) { $CalParam | Add-Member -NotePropertyName 'SharingPermissionFlags' -NotePropertyValue 'Delegate,CanViewPrivateItems' } - + if ($RemoveAccess) { - if ($PSCmdlet.ShouldProcess("$UserID\$folderName", "Remove permissions for $LoggingName")) { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = "$($UserID):\$folderName"; User = $RemoveAccess } + if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Remove permissions for $LoggingName")) { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = "$($UserID):\$FolderName"; User = $RemoveAccess } $Result = "Successfully removed access for $LoggingName from calendar $($CalParam.Identity)" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info } } else { - if ($PSCmdlet.ShouldProcess("$UserID\$folderName", "Set permissions for $LoggingName to $Permissions")) { + if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Set permissions for $LoggingName to $Permissions")) { try { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $CalParam -Anchor $UserID } catch { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $CalParam -Anchor $UserID } - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully set Calendar permissions $Permissions for $LoggingName on $UserID." -sev Info $Result = "Successfully set permissions on folder $($CalParam.Identity). The user $LoggingName now has $Permissions permissions on this folder." if ($CanViewPrivateItems) { - $Result += " The user can also view private items." + $Result += ' The user can also view private items.' } + if ($SendNotificationToUser) { + $Result += ' A notification has been sent to the user.' + } + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info } } } catch { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 8ff864589e35..412c379e377a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'ConditionalAccess' $Table = Get-CippTable -tablename 'templates' $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') - $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') + $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog if ($TestResult -eq $false) { #writing to each item that the license is not present. $settings.TemplateList | ForEach-Object { @@ -59,6 +59,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { if ($Policy.conditions.userRiskLevels.count -gt 0 -or $Policy.conditions.signInRiskLevels.count -gt 0) { if (!$TestP2) { Write-Information "Skipping policy $($Policy.displayName) as it requires AAD Premium P2 license." + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Policy.displayName) requires AAD Premium P2 license." -Tenant $Tenant continue } } diff --git a/Modules/CippExtensions/Private/Get-AssignedMap.ps1 b/Modules/CippExtensions/Private/Get-AssignedMap.ps1 new file mode 100644 index 000000000000..62dcab8ae20a --- /dev/null +++ b/Modules/CippExtensions/Private/Get-AssignedMap.ps1 @@ -0,0 +1,32 @@ +function Get-AssignedMap { + # Assigned Licenses Map + $AssignedMap = [pscustomobject]@{ + 'AADPremiumService' = 'o-skypeforbusiness' + 'MultiFactorService' = 'o-skypeforbusiness' + 'RMSOnline' = 'o-skypeforbusiness' + 'MicrosoftPrint' = 'o-yammer' + 'WindowsDefenderATP' = 'o-skypeforbusiness' + 'exchange' = 'o-exchange' + 'ProcessSimple' = 'o-onedrive' + 'OfficeForms' = 'o-yammer' + 'SCO' = 'o-skypeforbusiness' + 'MicrosoftKaizala' = 'o-yammer' + 'Adallom' = 'o-skypeforbusiness' + 'ProjectWorkManagement' = 'o-yammer' + 'TeamspaceAPI' = 'o-teams' + 'MicrosoftOffice' = 'o-yammer' + 'PowerAppsService' = 'o-onedrive' + 'SharePoint' = 'o-sharepoint' + 'MicrosoftCommunicationsOnline' = 'o-teams' + 'Deskless' = 'o-yammer' + 'MicrosoftStream' = 'o-yammer' + 'Sway' = 'o-yammer' + 'To-Do' = 'o-yammer' + 'WhiteboardServices' = 'o-yammer' + 'Windows' = 'o-skypeforbusiness' + 'YammerEnterprise' = 'o-yammer' + } + + return $AssignedMap + +} diff --git a/Modules/CippExtensions/Private/Get-AssignedNameMap.ps1 b/Modules/CippExtensions/Private/Get-AssignedNameMap.ps1 new file mode 100644 index 000000000000..fcb846e1ec82 --- /dev/null +++ b/Modules/CippExtensions/Private/Get-AssignedNameMap.ps1 @@ -0,0 +1,32 @@ +function Get-AssignedNameMap { + + $AssignedNameMap = @{ + 'AADPremiumService' = 'Azure Active Directory Premium' + 'MultiFactorService' = 'Azure Multi-Factor Authentication' + 'RMSOnline' = 'Azure Rights Management' + 'MicrosoftPrint' = 'Cloud Print' + 'WindowsDefenderATP' = 'Defender for Endpoint' + 'exchange' = 'Exchange Online' + 'ProcessSimple' = 'Flow' + 'OfficeForms' = 'Forms' + 'SCO' = 'Intune' + 'MicrosoftKaizala' = 'Kaizala' + 'Adallom' = 'Microsoft Cloud App Security' + 'ProjectWorkManagement' = 'Microsoft Planner' + 'TeamspaceAPI' = 'Microsoft Teams' + 'MicrosoftOffice' = 'Office 365' + 'PowerAppsService' = 'PowerApps' + 'SharePoint' = 'SharePoint Online' + 'MicrosoftCommunicationsOnline' = 'Skype for Business' + 'Deskless' = 'Staff Hub' + 'MicrosoftStream' = 'Stream' + 'Sway' = 'Sway' + 'To-Do' = 'To-Do' + 'WhiteboardServices' = 'Whiteboard' + 'Windows' = 'Windows' + 'YammerEnterprise' = 'Yammer' + } + + return $AssignedNameMap + +} diff --git a/Modules/CippExtensions/Public/HIBP/New-BreachTenantSearch.ps1 b/Modules/CippExtensions/Public/HIBP/New-BreachTenantSearch.ps1 index e387161ad89e..b75c3e891f37 100644 --- a/Modules/CippExtensions/Public/HIBP/New-BreachTenantSearch.ps1 +++ b/Modules/CippExtensions/Public/HIBP/New-BreachTenantSearch.ps1 @@ -6,7 +6,7 @@ function New-BreachTenantSearch { ) $Table = Get-CIPPTable -TableName UserBreaches - $LatestBreach = Get-BreachInfo -TenantFilter $TenantFilter | Group-Object -Property clientDomain + $LatestBreach = Get-BreachInfo -TenantFilter $TenantFilter | Where-Object { $_.email } | Group-Object -Property clientDomain $usersResults = foreach ($domain in $LatestBreach) { $ExistingBreaches = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$($domain.name)'" diff --git a/Modules/CippExtensions/Public/Halo/Get-HaloTicketOutcome.ps1 b/Modules/CippExtensions/Public/Halo/Get-HaloTicketOutcome.ps1 new file mode 100644 index 000000000000..1ae15d4e4e5b --- /dev/null +++ b/Modules/CippExtensions/Public/Halo/Get-HaloTicketOutcome.ps1 @@ -0,0 +1,44 @@ +function Get-HaloTicketOutcome { + <# + .SYNOPSIS + Get Halo Ticket Outcome + .DESCRIPTION + Get Halo Ticket Outcome + .EXAMPLE + Get-HaloTicketOutcome + + #> + [CmdletBinding()] + param () + $Table = Get-CIPPTable -TableName Extensionsconfig + try { + $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).HaloPSA + $Token = Get-HaloToken -configuration $Configuration + $TicketType = $Configuration.TicketType.value ?? $Configuration.TicketType + if ($TicketType) { + $WorkflowId = (Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/tickettype/$TicketType" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($Token.access_token)" }).workflow_id + $Workflow = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/workflow/$WorkflowId" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($Token.access_token)" } + $Outcomes = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/outcome" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($Token.access_token)" } + $Outcomes | Where-Object { $_.id -in $Workflow.steps.actions.action_id } | Sort-Object -Property buttonname + } + else { + # Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/outcome" -ContentType 'application/json' -Method GET -Headers @{Authorization = "Bearer $($Token.access_token)" } + @( + @{ + buttonname = 'Select and save a Ticket Type first to see available outcomes' + value = -1 + } + ) + } + } + catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } + else { + $_.Exception.message + } + @(@{name = "Could not get HaloPSA Outcomes, error: $Message"; id = '' }) + } +} + diff --git a/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 b/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 index 537b6a72f438..0249248c88ec 100644 --- a/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 +++ b/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 @@ -1,110 +1,124 @@ function New-HaloPSATicket { - [CmdletBinding(SupportsShouldProcess)] - param ( - $title, - $description, - $client - ) - #Get HaloPSA Token based on the config we have. - $Table = Get-CIPPTable -TableName Extensionsconfig - $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json).HaloPSA - $TicketTable = Get-CIPPTable -TableName 'PSATickets' - $token = Get-HaloToken -configuration $Configuration - # sha hash title - $TitleHash = Get-StringHash -String $title + [CmdletBinding(SupportsShouldProcess)] + param ( + $title, + $description, + $client + ) + #Get HaloPSA Token based on the config we have. + $Table = Get-CIPPTable -TableName Extensionsconfig + $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json).HaloPSA + $TicketTable = Get-CIPPTable -TableName 'PSATickets' + $token = Get-HaloToken -configuration $Configuration + # sha hash title + $TitleHash = Get-StringHash -String $title - if ($Configuration.ConsolidateTickets) { - $ExistingTicket = Get-CIPPAzDataTableEntity @TicketTable -Filter "PartitionKey eq 'HaloPSA' and RowKey eq '$($client)-$($TitleHash)'" - if ($ExistingTicket) { - Write-Information "Ticket already exists in HaloPSA: $($ExistingTicket.TicketID)" + if ($Configuration.ConsolidateTickets) { + $ExistingTicket = Get-CIPPAzDataTableEntity @TicketTable -Filter "PartitionKey eq 'HaloPSA' and RowKey eq '$($client)-$($TitleHash)'" + if ($ExistingTicket) { + Write-Information "Ticket already exists in HaloPSA: $($ExistingTicket.TicketID)" - $Ticket = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Tickets/$($ExistingTicket.TicketID)?includedetails=true&includelastaction=false&nocache=undefined&includeusersassets=false&isdetailscreen=true" -ContentType 'application/json; charset=utf-8' -Method Get -Headers @{Authorization = "Bearer $($token.access_token)" } - if (!$Ticket.hasbeenclosed) { - Write-Information 'Ticket is still open, adding new note' - $Object = [PSCustomObject]@{ - ticket_id = $ExistingTicket.TicketID - outcome = 'Private Note' - outcome_id = 7 - hiddenfromuser = $true - note_html = $description - } - $body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) - try { - if ($PSCmdlet.ShouldProcess('Add note to HaloPSA ticket', 'Add note')) { - $Action = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/actions" -ContentType 'application/json; charset=utf-8' -Method Post -Body $body -Headers @{Authorization = "Bearer $($token.access_token)" } - Write-Information "Note added to ticket in HaloPSA: $($ExistingTicket.TicketID)" - } - return "Note added to ticket in HaloPSA: $($ExistingTicket.TicketID)" - } catch { - $Message = if ($_.ErrorDetails.Message) { - Get-NormalizedError -Message $_.ErrorDetails.Message - } else { - $_.Exception.message - } - Write-LogMessage -message "Failed to add note to HaloPSA ticket: $Message" -API 'HaloPSATicket' -sev Error -LogData (Get-CippException -Exception $_) - Write-Information "Failed to add note to HaloPSA ticket: $Message" - Write-Information "Body we tried to ship: $body" - return "Failed to add note to HaloPSA ticket: $Message" - } + $Ticket = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Tickets/$($ExistingTicket.TicketID)?includedetails=true&includelastaction=false&nocache=undefined&includeusersassets=false&isdetailscreen=true" -ContentType 'application/json; charset=utf-8' -Method Get -Headers @{Authorization = "Bearer $($token.access_token)" } -SkipHttpErrorCheck + if ($Ticket.id) { + if (!$Ticket.hasbeenclosed) { + Write-Information 'Ticket is still open, adding new note' + $Object = [PSCustomObject]@{ + ticket_id = $ExistingTicket.TicketID + outcome_id = 7 + hiddenfromuser = $true + note_html = $description + } + + if ($Configuration.Outcome) { + $Outcome = $Configuration.Outcome.value ?? $Configuration.Outcome + $Object.outcome_id = $Outcome + } + + $body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) + try { + if ($PSCmdlet.ShouldProcess('Add note to HaloPSA ticket', 'Add note')) { + $Action = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/actions" -ContentType 'application/json; charset=utf-8' -Method Post -Body $body -Headers @{Authorization = "Bearer $($token.access_token)" } + Write-Information "Note added to ticket in HaloPSA: $($ExistingTicket.TicketID)" } + return "Note added to ticket in HaloPSA: $($ExistingTicket.TicketID)" + } + catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } + else { + $_.Exception.message + } + Write-LogMessage -message "Failed to add note to HaloPSA ticket: $Message" -API 'HaloPSATicket' -sev Error -LogData (Get-CippException -Exception $_) + Write-Information "Failed to add note to HaloPSA ticket: $Message" + Write-Information "Body we tried to ship: $body" + return "Failed to add note to HaloPSA ticket: $Message" + } } + } + else { + Write-Information 'Existing ticket could not be found. Creating a new ticket instead.' + } } + } - $Object = [PSCustomObject]@{ - files = $null - usertype = 1 - userlookup = @{ - id = -1 - lookupdisplay = 'Enter Details Manually' - } - client_id = ($client | Select-Object -Last 1) - _forcereassign = $true - site_id = $null - user_name = $null - reportedby = $null - summary = $title - details_html = $description - donotapplytemplateintheapi = $true - attachments = @() - _novalidate = $true + $Object = [PSCustomObject]@{ + files = $null + usertype = 1 + userlookup = @{ + id = -1 + lookupdisplay = 'Enter Details Manually' } + client_id = ($client | Select-Object -Last 1) + _forcereassign = $true + site_id = $null + user_name = $null + reportedby = $null + summary = $title + details_html = $description + donotapplytemplateintheapi = $true + attachments = @() + _novalidate = $true + } - if ($Configuration.TicketType) { - $TicketType = $Configuration.TicketType.value ?? $Configuration.TicketType - $object | Add-Member -MemberType NoteProperty -Name 'tickettype_id' -Value $TicketType -Force - } - #use the token to create a new ticket in HaloPSA - $body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) + if ($Configuration.TicketType) { + $TicketType = $Configuration.TicketType.value ?? $Configuration.TicketType + $object | Add-Member -MemberType NoteProperty -Name 'tickettype_id' -Value $TicketType -Force + } + #use the token to create a new ticket in HaloPSA + $body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) - Write-Information 'Sending ticket to HaloPSA' - Write-Information $body - try { - if ($PSCmdlet.ShouldProcess('Send ticket to HaloPSA', 'Create ticket')) { - $Ticket = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Tickets" -ContentType 'application/json; charset=utf-8' -Method Post -Body $body -Headers @{Authorization = "Bearer $($token.access_token)" } - Write-Information "Ticket created in HaloPSA: $($Ticket.id)" + Write-Information 'Sending ticket to HaloPSA' + Write-Information $body + try { + if ($PSCmdlet.ShouldProcess('Send ticket to HaloPSA', 'Create ticket')) { + $Ticket = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Tickets" -ContentType 'application/json; charset=utf-8' -Method Post -Body $body -Headers @{Authorization = "Bearer $($token.access_token)" } + Write-Information "Ticket created in HaloPSA: $($Ticket.id)" - if ($Configuration.ConsolidateTickets) { - $TicketObject = [PSCustomObject]@{ - PartitionKey = 'HaloPSA' - RowKey = "$($client)-$($TitleHash)" - Title = $title - ClientId = $client - TicketID = $Ticket.id - } - Add-CIPPAzDataTableEntity @TicketTable -Entity $TicketObject -Force - Write-Information 'Ticket added to consolidation table' - } - return "Ticket created in HaloPSA: $($Ticket.id)" - } - } catch { - $Message = if ($_.ErrorDetails.Message) { - Get-NormalizedError -Message $_.ErrorDetails.Message - } else { - $_.Exception.message + if ($Configuration.ConsolidateTickets) { + $TicketObject = [PSCustomObject]@{ + PartitionKey = 'HaloPSA' + RowKey = "$($client)-$($TitleHash)" + Title = $title + ClientId = $client + TicketID = $Ticket.id } - Write-LogMessage -message "Failed to send ticket to HaloPSA: $Message" -API 'HaloPSATicket' -sev Error -LogData (Get-CippException -Exception $_) - Write-Information "Failed to send ticket to HaloPSA: $Message" - Write-Information "Body we tried to ship: $body" - return "Failed to send ticket to HaloPSA: $Message" + Add-CIPPAzDataTableEntity @TicketTable -Entity $TicketObject -Force + Write-Information 'Ticket added to consolidation table' + } + return "Ticket created in HaloPSA: $($Ticket.id)" + } + } + catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } + else { + $_.Exception.message } + Write-LogMessage -message "Failed to send ticket to HaloPSA: $Message" -API 'HaloPSATicket' -sev Error -LogData (Get-CippException -Exception $_) + Write-Information "Failed to send ticket to HaloPSA: $Message" + Write-Information "Body we tried to ship: $body" + return "Failed to send ticket to HaloPSA: $Message" + } } diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 index 9e0b5cda5eb6..867c6fd8082e 100644 --- a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -3,14 +3,14 @@ function Invoke-HuduExtensionSync { .FUNCTIONALITY Internal #> - Param( + param( $Configuration, $TenantFilter ) try { Connect-HuduAPI -configuration $Configuration $Configuration = $Configuration.Hudu - + $Tenant = Get-Tenants -TenantFilter $TenantFilter -IncludeErrors $CompanyResult = [PSCustomObject]@{ Name = $Tenant.displayName Users = 0 @@ -19,12 +19,14 @@ function Invoke-HuduExtensionSync { Logs = [System.Collections.Generic.List[string]]@() } + $AssignedNameMap = Get-AssignedNameMap + $AssignedMap = Get-AssignedMap + # Get mapping configuration $MappingTable = Get-CIPPTable -TableName 'CippMapping' $Mappings = Get-CIPPAzDataTableEntity @MappingTable -Filter "PartitionKey eq 'HuduMapping' or PartitionKey eq 'HuduFieldMapping'" $defaultdomain = $TenantFilter - $Tenant = Get-Tenants -IncludeErrors | Where-Object { $_.defaultDomainName -eq $TenantFilter } $TenantMap = $Mappings | Where-Object { $_.RowKey -eq $Tenant.customerId } # Get Asset cache @@ -81,7 +83,6 @@ function Invoke-HuduExtensionSync { Write-Host "Configuration: $($Configuration | ConvertTo-Json)" - try { if (![string]::IsNullOrEmpty($DeviceLayoutId)) { $null = Add-HuduAssetLayoutField -AssetLayoutId $DeviceLayoutId @@ -168,7 +169,7 @@ function Invoke-HuduExtensionSync { # Get members from cache $Members = ($ExtensionCache."AllRoles_$($Role.id)") [PSCustomObject]@{ - ID = $Result.id + ID = $Role.id DisplayName = $Role.displayName Description = $Role.description Members = $Members @@ -253,22 +254,22 @@ function Invoke-HuduExtensionSync { $DeviceCompliancePolicies = $ExtensionCache.DeviceCompliancePolicies $DeviceComplianceDetails = foreach ($Policy in $DeviceCompliancePolicies) { - $DeviceStatuses = $ExtensionCache."DeviceCompliancePolicy_$($Policy.id)" + $DeviceStatuses = $ExtensionCache."DeviceCompliancePolicies_$($Policy.id)" [pscustomobject]@{ ID = $Policy.id DisplayName = $Policy.displayName - DeviceStatuses = $DeviceStatuses + DeviceStatuses = @($DeviceStatuses) } } $AllGroups = $ExtensionCache.Groups $Groups = foreach ($Group in $AllGroups) { - $Members = $ExtensionCache."Groups_$($Result.id)" + $Members = $ExtensionCache."Groups_$($Group.id)" [pscustomobject]@{ ID = $Group.id DisplayName = $Group.displayName - Members = $Members + Members = @($Members) } } @@ -313,10 +314,66 @@ function Invoke-HuduExtensionSync { } } + # Enhanced policy information extraction based on API structure [pscustomobject]@{ - ID = $CAPolicy.id - DisplayName = $CAPolicy.displayName - Members = $CAMembers + ID = $CAPolicy.id + DisplayName = $CAPolicy.displayName + State = $CAPolicy.state + CreatedDateTime = $CAPolicy.createdDateTime + ModifiedDateTime = $CAPolicy.modifiedDateTime + Members = @($CAMembers) + + # Applications conditions + IncludeApplications = if ($CAPolicy.conditions.applications.includeApplications) { + $CAPolicy.conditions.applications.includeApplications -join ', ' + } else { 'None' } + ExcludeApplications = if ($CAPolicy.conditions.applications.excludeApplications) { + $CAPolicy.conditions.applications.excludeApplications -join ', ' + } else { 'None' } + + # Location conditions + IncludeLocations = if ($CAPolicy.conditions.locations.includeLocations) { + $CAPolicy.conditions.locations.includeLocations -join ', ' + } else { 'None' } + ExcludeLocations = if ($CAPolicy.conditions.locations.excludeLocations) { + $CAPolicy.conditions.locations.excludeLocations -join ', ' + } else { 'None' } + + # Platform conditions + Platforms = if ($CAPolicy.conditions.platforms -and $CAPolicy.conditions.platforms.includePlatforms) { + $CAPolicy.conditions.platforms.includePlatforms -join ', ' + } else { 'All' } + + # Client app types + ClientAppTypes = if ($CAPolicy.conditions.clientAppTypes) { + $CAPolicy.conditions.clientAppTypes -join ', ' + } else { 'All' } + + # Grant controls + GrantOperator = $CAPolicy.grantControls.operator + BuiltInControls = if ($CAPolicy.grantControls.builtInControls) { + $CAPolicy.grantControls.builtInControls -join ', ' + } else { 'None' } + AuthenticationStrength = if ($CAPolicy.grantControls.authenticationStrength) { + $CAPolicy.grantControls.authenticationStrength.displayName + } else { 'None' } + + # Session controls + SignInFrequency = if ($CAPolicy.sessionControls -and $CAPolicy.sessionControls.signInFrequency -and $CAPolicy.sessionControls.signInFrequency.isEnabled) { + "$($CAPolicy.sessionControls.signInFrequency.value) $($CAPolicy.sessionControls.signInFrequency.type)" + } else { 'Not configured' } + + PersistentBrowser = if ($CAPolicy.sessionControls -and $CAPolicy.sessionControls.persistentBrowser) { + $CAPolicy.sessionControls.persistentBrowser.mode + } else { 'Not configured' } + + # Risk levels + UserRiskLevels = if ($CAPolicy.conditions.userRiskLevels) { + $CAPolicy.conditions.userRiskLevels -join ', ' + } else { 'None' } + SignInRiskLevels = if ($CAPolicy.conditions.signInRiskLevels) { + $CAPolicy.conditions.signInRiskLevels -join ', ' + } else { 'None' } } } @@ -380,10 +437,17 @@ function Invoke-HuduExtensionSync { $UserPolicies = foreach ($cap in $ConditionalAccessMembers) { if ($User.id -in $Cap.Members) { - $temp = [PSCustomObject]@{ - displayName = $cap.displayName + [PSCustomObject]@{ + displayName = $cap.displayName + state = $cap.State + authenticationStrength = $cap.AuthenticationStrength + clientAppTypes = $cap.ClientAppTypes + includeApplications = $cap.IncludeApplications + includeLocations = $cap.IncludeLocations + signInFrequency = $cap.SignInFrequency + userRiskLevels = $cap.UserRiskLevels + signInRiskLevels = $cap.SignInRiskLevels } - $temp } } @@ -396,7 +460,6 @@ function Invoke-HuduExtensionSync { $MailboxDetailedRequest = $MailboxDetailedFull | Where-Object { $_.Id -eq $User.id } $StatsRequest = $MailboxStatsFull | Where-Object { $_.'userPrincipalName' -eq $User.userPrincipalName } - $PermsRequest = $Permissions | Where-Object { $_.Identity -eq $User.id } $ParsedPerms = foreach ($Perm in $PermsRequest) { @@ -426,10 +489,11 @@ function Invoke-HuduExtensionSync { MailboxPopEnabled = $CASRequest.PopEnabled MailboxActiveSyncEnabled = $CASRequest.ActiveSyncEnabled Permissions = $ParsedPerms - ProhibitSendQuota = [math]::Round([float]($MailboxDetailedRequest.ProhibitSendQuota -split ' GB')[0], 2) - ProhibitSendReceiveQuota = [math]::Round([float]($MailboxDetailedRequest.ProhibitSendReceiveQuota -split ' GB')[0], 2) + ProhibitSendQuota = $StatsRequest.prohibitSendQuotaInBytes + ProhibitSendReceiveQuota = $StatsRequest.prohibitSendReceiveQuotaInBytes ItemCount = [math]::Round($StatsRequest.'itemCount', 2) - TotalItemSize = $TotalItemSize + TotalItemSize = $StatsRequest.totalItemSize + StorageUsedInBytes = $StatsRequest.storageUsedInBytes } $userDevices = ($devices | Where-Object { $_.userPrincipalName -eq $user.userPrincipalName } | Select-Object @{N = 'Name'; E = { "$($_.deviceName) ($($_.operatingSystem))" } }).name -join '
' @@ -509,32 +573,50 @@ function Invoke-HuduExtensionSync { $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Permissions' -Value "$($UserMailSettings.Permissions | ConvertTo-Html -Fragment | Out-String)")) - $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Prohibit Send Quota' -Value "$($UserMailSettings.ProhibitSendQuota)")) - $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Prohibit Send Receive Quota' -Value "$($UserMailSettings.ProhibitSendReceiveQuota)")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Item Count' -Value "$($UserMailSettings.ItemCount)")) - $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Total Mailbox Size' -Value "$($UserMailSettings.TotalItemSize)")) + try { - $UserMailboxUsePercent = [math]::Round([float](($UserMailSettings.TotalItemSize / $UserMailSettings.ProhibitSendReceiveQuota) * 100), 2) + $UserMailboxUsePercent = [math]::Round([float](($UserMailSettings.StorageUsedInBytes / $UserMailSettings.prohibitSendReceiveQuota) * 100), 2) + $MailboxStorageUsed = [math]::Round($UserMailSettings.StorageUsedInBytes / 1024 / 1024 / 1024, 2) + $MailboxStorageAllocated = [math]::Round($UserMailSettings.prohibitSendReceiveQuota / 1024 / 1024 / 1024, 2) + $MailboxProhibitSendQuota = [math]::Round($UserMailSettings.ProhibitSendQuota / 1024 / 1024 / 1024, 2) } catch { $UserMailboxUsePercent = 100 + $MailboxStorageUsed = 0 + $MailboxStorageAllocated = 0 } + + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Prohibit Send Quota' -Value "$($MailboxProhibitSendQuota) GB")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Prohibit Send Receive Quota' -Value "$($MailboxStorageAllocated) GB")) + $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Total Mailbox Size' -Value "$($MailboxStorageUsed) GB")) + $UserMailboxUsage = @"
-
$([math]::Round($UserMailSettings.TotalItemSize,2)) GB used, $UserMailboxUsePercent% of $([math]::Round($UserMailSettings.ProhibitSendReceiveQuota, 2)) GB
+
$MailboxStorageUsed GB used, $UserMailboxUsePercent% of $MailboxStorageAllocated GB
"@ $UserMailboxDetailsFormatted.add($(Get-HuduFormattedField -Title 'Mailbox Usage' -Value $UserMailboxUsage)) } - $UserPoliciesFormatted = '' [System.Collections.Generic.List[PSCustomObject]]$UserOverviewFormatted = @() $UserOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Name' -Value "$($User.displayName)")) @@ -685,6 +767,8 @@ function Invoke-HuduExtensionSync { } } catch { $CompanyResult.Errors.add("User $($User.userPrincipalName): A fatal error occured while processing user $_") + Write-Warning "User $($User.userPrincipalName): A fatal error occured while processing user $_" + Write-Information $_.InvocationInfo.PositionMessage } } @@ -692,67 +776,112 @@ function Invoke-HuduExtensionSync { } - $CompanyResult.Logs.Add('Starting Device Processing') - foreach ($Device in $Devices) { - try { - [System.Collections.Generic.List[PSCustomObject]]$DeviceOverviewFormatted = @() - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Device Name' -Value "$($Device.deviceName)")) - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'User' -Value "$($Device.userDisplayName)")) - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Email' -Value "$($Device.userPrincipalName)")) - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Owner' -Value "$($Device.ownerType)")) - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Enrolled' -Value "$($Device.enrolledDateTime)")) - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Last Checkin' -Value "$($Device.lastSyncDateTime)")) - if ($Device.complianceState -eq 'compliant') { - $CompliantSymbol = '   ' - } else { - $CompliantSymbol = '   ' - } - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Compliant' -Value "$($CompliantSymbol)$($Device.complianceState)")) - $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Management Type' -Value "$($Device.managementAgent)")) - - [System.Collections.Generic.List[PSCustomObject]]$DeviceHardwareFormatted = @() - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Serial Number' -Value "$($Device.serialNumber)")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'OS' -Value "$($Device.operatingSystem)")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'OS Versions' -Value "$($Device.osVersion)")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Chassis' -Value "$($Device.chassisType)")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Model' -Value "$($Device.model)")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Manufacturer' -Value "$($Device.manufacturer)")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Total Storage' -Value "$([math]::Round($Device.totalStorageSpaceInBytes /1024 /1024 /1024, 2))")) - $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Free Storage' -Value "$([math]::Round($Device.freeStorageSpaceInBytes /1024 /1024 /1024, 2))")) - - [System.Collections.Generic.List[PSCustomObject]]$DeviceEnrollmentFormatted = @() - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Enrollment Type' -Value "$($Device.deviceEnrollmentType)")) - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Join Type' -Value "$($Device.joinType)")) - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Registration State' -Value "$($Device.deviceRegistrationState)")) - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Autopilot Enrolled' -Value "$($Device.autopilotEnrolled)")) - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Device Guard Requirements' -Value "$($Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityHardwareRequirementState)")) - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Virtualistation Based Security' -Value "$($Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityState)")) - $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Credential Guard' -Value "$($Device.hardwareinformation.deviceGuardLocalSystemAuthorityCredentialGuardState)")) - - $DevicePoliciesTable = foreach ($Policy in $DeviceComplianceDetails) { - if ($device.deviceName -in $Policy.DeviceStatuses.deviceDisplayName) { - $Status = $Policy.DeviceStatuses | Where-Object { $_.deviceDisplayName -eq $device.deviceName } - if ($Status.status -ne 'unknown') { - [PSCustomObject]@{ - Name = $Policy.displayName - Status = ($Status.status | Select-Object -Unique) -join ', ' - 'Last Report' = "$(Get-Date($Status.lastReportedDateTime[0]) -Format 'yyyy-MM-dd HH:mm:ss')" - 'Grace Expiry' = "$(Get-Date($Status.complianceGracePeriodExpirationDateTime[0]) -Format 'yyyy-MM-dd HH:mm:ss')" + if (![string]::IsNullOrEmpty($DeviceLayoutId)) { + $CompanyResult.Logs.Add('Starting Device Processing') + Write-Information "### Processing Devices for $($Tenant.defaultDomainName)" + foreach ($Device in $Devices) { + try { + [System.Collections.Generic.List[PSCustomObject]]$DeviceOverviewFormatted = @() + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Device Name' -Value "$($Device.deviceName)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'User' -Value "$($Device.userDisplayName)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'User Email' -Value "$($Device.userPrincipalName)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Owner' -Value "$($Device.ownerType)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Enrolled' -Value "$($Device.enrolledDateTime)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Last Checkin' -Value "$($Device.lastSyncDateTime)")) + if ($Device.complianceState -eq 'compliant') { + $CompliantSymbol = '   ' + } else { + $CompliantSymbol = '   ' + } + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Compliant' -Value "$($CompliantSymbol)$($Device.complianceState)")) + $DeviceOverviewFormatted.add($(Get-HuduFormattedField -Title 'Management Type' -Value "$($Device.managementAgent)")) + + [System.Collections.Generic.List[PSCustomObject]]$DeviceHardwareFormatted = @() + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Serial Number' -Value "$($Device.serialNumber)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'OS' -Value "$($Device.operatingSystem)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'OS Versions' -Value "$($Device.osVersion)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Chassis' -Value "$($Device.chassisType)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Model' -Value "$($Device.model)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Manufacturer' -Value "$($Device.manufacturer)")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Total Storage' -Value "$([math]::Round($Device.totalStorageSpaceInBytes /1024 /1024 /1024, 2))")) + $DeviceHardwareFormatted.add($(Get-HuduFormattedField -Title 'Free Storage' -Value "$([math]::Round($Device.freeStorageSpaceInBytes /1024 /1024 /1024, 2))")) + + [System.Collections.Generic.List[PSCustomObject]]$DeviceEnrollmentFormatted = @() + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Enrollment Type' -Value "$($Device.deviceEnrollmentType)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Join Type' -Value "$($Device.joinType)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Registration State' -Value "$($Device.deviceRegistrationState)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Autopilot Enrolled' -Value "$($Device.autopilotEnrolled)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Device Guard Requirements' -Value "$($Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityHardwareRequirementState)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Virtualistation Based Security' -Value "$($Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityState)")) + $DeviceEnrollmentFormatted.add($(Get-HuduFormattedField -Title 'Credential Guard' -Value "$($Device.hardwareinformation.deviceGuardLocalSystemAuthorityCredentialGuardState)")) + + $DevicePoliciesTable = foreach ($Policy in $DeviceComplianceDetails) { + # Handle DeviceStatuses as either array or single object + $DeviceStatuses = $Policy.DeviceStatuses + + # Enhanced device matching with multiple strategies + $MatchingStatuses = $DeviceStatuses | Where-Object { + # Primary match: deviceDisplayName to deviceName (most reliable) + ($_.deviceDisplayName -eq $device.deviceName) -or + # Secondary match: deviceDisplayName to managedDeviceName + ($_.deviceDisplayName -eq $device.managedDeviceName) -or + # Tertiary match: extract device ID from composite compliance ID and match to device.id + ($_.id -and $device.id -and $_.id -match ".*_$([regex]::Escape($device.id))$") -or + # Quaternary match: extract device ID from composite compliance ID and match to azureADDeviceId + ($_.id -and $device.azureADDeviceId -and $_.id -match ".*_$([regex]::Escape($device.azureADDeviceId))$") -or + # Alternative match: check if azureADDeviceId appears anywhere in the compliance ID + ($_.id -and $device.azureADDeviceId -and $_.id -like "*$($device.azureADDeviceId)*") + } + + if ($MatchingStatuses) { + foreach ($Status in $MatchingStatuses) { + Write-Information "Processing Status for Device $($device.deviceName), Policy $($Policy.displayName)" + # Filter out invalid statuses + if ($Status.status -and $Status.status -ne 'unknown' -and $Status.status -ne $null) { + try { + $LastReport = if ($Status.lastReportedDateTime) { + (Get-Date $Status.lastReportedDateTime -Format 'yyyy-MM-dd HH:mm:ss') + } else { 'N/A' } + + $GraceExpiry = if ($Status.complianceGracePeriodExpirationDateTime) { + (Get-Date $Status.complianceGracePeriodExpirationDateTime -Format 'yyyy-MM-dd HH:mm:ss') + } else { 'N/A' } + + [PSCustomObject]@{ + Name = $Policy.displayName + Status = $Status.status + 'Last Report' = $LastReport + 'Grace Expiry' = $GraceExpiry + 'Match Method' = if ($Status.deviceDisplayName -eq $device.deviceName) { 'Device Name' } + elseif ($Status.deviceDisplayName -eq $device.managedDeviceName) { 'Managed Name' } + else { 'Device ID' } + } + } catch { + # Log but continue processing if date parsing fails + Write-Warning "Failed to parse compliance policy dates for device $($device.deviceName), policy $($Policy.displayName): $_" + [PSCustomObject]@{ + Name = $Policy.displayName + Status = $Status.status + 'Last Report' = 'Parse Error' + 'Grace Expiry' = 'Parse Error' + 'Match Method' = 'Error' + } + } + } } } } - } - $DevicePoliciesFormatted = $DevicePoliciesTable | ConvertTo-Html -Fragment | Out-String + $DevicePoliciesFormatted = $DevicePoliciesTable | ConvertTo-Html -Fragment | Out-String - $DeviceGroupsTable = foreach ($Group in $Groups) { - if ($device.azureADDeviceId -in $Group.members.deviceId) { - [PSCustomObject]@{ - Name = $Group.displayName + $DeviceGroupsTable = foreach ($Group in $Groups) { + if ($device.azureADDeviceId -in $Group.members.deviceId) { + [PSCustomObject]@{ + Name = $Group.displayName + } } } - } - $DeviceGroupsFormatted = $DeviceGroupsTable | ConvertTo-Html -Fragment | Out-String - <# + $DeviceGroupsFormatted = $DeviceGroupsTable | ConvertTo-Html -Fragment | Out-String + <# $DeviceAppsTable = foreach ($App in $DeviceAppInstallDetails) { if ($device.id -in $App.InstalledAppDetails.deviceId) { $Status = $App.InstalledAppDetails | Where-Object { $_.deviceId -eq $device.id } @@ -764,117 +893,125 @@ function Invoke-HuduExtensionSync { } $DeviceAppsFormatted = $DeviceAppsTable | ConvertTo-Html -Fragment | Out-String #> - $DeviceOverviewBlock = Get-HuduFormattedBlock -Heading 'Device Details' -Body ($DeviceOverviewFormatted -join '') - $DeviceHardwareBlock = Get-HuduFormattedBlock -Heading 'Hardware Details' -Body ($DeviceHardwareFormatted -join '') - $DeviceEnrollmentBlock = Get-HuduFormattedBlock -Heading 'Device Enrollment Details' -Body ($DeviceEnrollmentFormatted -join '') - $DevicePolicyBlock = Get-HuduFormattedBlock -Heading 'Compliance Policies' -Body ($DevicePoliciesFormatted -join '') - #$DeviceAppsBlock = Get-HuduFormattedBlock -Heading 'App Details' -Body ($DeviceAppsFormatted -join '') - $DeviceGroupsBlock = Get-HuduFormattedBlock -Heading 'Device Groups' -Body ($DeviceGroupsFormatted -join '') - - if ("$($device.serialNumber)" -in $ExcludeSerials) { - $HuduDevice = $HuduDevices | Where-Object { $_.name -eq $device.deviceName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.name -contains $device.deviceName) } - } else { - $HuduDevice = $HuduDevices | Where-Object { $_.primary_serial -eq $device.serialNumber -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.serialNumber -eq $device.serialNumber) } - if (!$HuduDevice) { + $DeviceOverviewBlock = Get-HuduFormattedBlock -Heading 'Device Details' -Body ($DeviceOverviewFormatted -join '') + $DeviceHardwareBlock = Get-HuduFormattedBlock -Heading 'Hardware Details' -Body ($DeviceHardwareFormatted -join '') + $DeviceEnrollmentBlock = Get-HuduFormattedBlock -Heading 'Device Enrollment Details' -Body ($DeviceEnrollmentFormatted -join '') + $DevicePolicyBlock = Get-HuduFormattedBlock -Heading 'Compliance Policies' -Body ($DevicePoliciesFormatted -join '') + #$DeviceAppsBlock = Get-HuduFormattedBlock -Heading 'App Details' -Body ($DeviceAppsFormatted -join '') + $DeviceGroupsBlock = Get-HuduFormattedBlock -Heading 'Device Groups' -Body ($DeviceGroupsFormatted -join '') + + if ("$($device.serialNumber)" -in $ExcludeSerials) { $HuduDevice = $HuduDevices | Where-Object { $_.name -eq $device.deviceName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.name -contains $device.deviceName) } + } else { + $HuduDevice = $HuduDevices | Where-Object { $_.primary_serial -eq $device.serialNumber -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.serialNumber -eq $device.serialNumber) } + if (!$HuduDevice) { + $HuduDevice = $HuduDevices | Where-Object { $_.name -eq $device.deviceName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.name -contains $device.deviceName) } + } } - } - [System.Collections.Generic.List[PSCustomObject]]$DeviceLinksFormatted = @() - $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "https://intune.microsoft.com/$($Tenant.defaultDomainName)/#blade/Microsoft_Intune_Devices/DeviceSettingsBlade/overview/mdmDeviceId/$($Device.id)" -Icon 'fas fa-laptop' -Title 'Endpoint Manager')) + [System.Collections.Generic.List[PSCustomObject]]$DeviceLinksFormatted = @() + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "https://intune.microsoft.com/$($Tenant.defaultDomainName)/#blade/Microsoft_Intune_Devices/DeviceSettingsBlade/overview/mdmDeviceId/$($Device.id)" -Icon 'fas fa-laptop' -Title 'Endpoint Manager')) - if ($HuduDevice) { - $DRMMCard = $HuduDevice.cards | Where-Object { $_.integrator_name -eq 'dattormm' } - if ($DRMMCard) { - $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "$($RMMDeviceURL)$($DRMMCard.data.id)" -Icon 'fas fa-laptop-code' -Title 'Datto RMM')) - $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "$($RMMRemoteURL)$($DRMMCard.data.id)" -Icon 'fas fa-desktop' -Title 'Datto RMM Remote')) - } - $ManageCard = $HuduDevice.cards | Where-Object { $_.integrator_name -eq 'cw_manage' } - if ($ManageCard) { - $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL $ManageCard.data.managementLink -Icon 'fas fa-laptop-code' -Title 'CW Automate')) - $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL $ManageCard.data.remoteLink -Icon 'fas fa-desktop' -Title 'CW Control')) + if ($HuduDevice) { + $DRMMCard = $HuduDevice.cards | Where-Object { $_.integrator_name -eq 'dattormm' } + if ($DRMMCard) { + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "$($RMMDeviceURL)$($DRMMCard.data.id)" -Icon 'fas fa-laptop-code' -Title 'Datto RMM')) + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL "$($RMMRemoteURL)$($DRMMCard.data.id)" -Icon 'fas fa-desktop' -Title 'Datto RMM Remote')) + } + $ManageCard = $HuduDevice.cards | Where-Object { $_.integrator_name -eq 'cw_manage' } + if ($ManageCard) { + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL $ManageCard.data.managementLink -Icon 'fas fa-laptop-code' -Title 'CW Automate')) + $DeviceLinksFormatted.add((Get-HuduLinkBlock -URL $ManageCard.data.remoteLink -Icon 'fas fa-desktop' -Title 'CW Control')) + } } - } - $DeviceLinksBlock = "
Management Links
$($DeviceLinksFormatted -join '')
" + $DeviceLinksBlock = "
Management Links
$($DeviceLinksFormatted -join '')
" - $DeviceIntuneDetailshtml = "
$DeviceLinksBlock
$($DeviceOverviewBlock)$($DeviceHardwareBlock)$($DeviceEnrollmentBlock)$($DevicePolicyBlock)$($DeviceAppsBlock)$($DeviceGroupsBlock)
" + $DeviceIntuneDetailshtml = "
$DeviceLinksBlock
$($DeviceOverviewBlock)$($DeviceHardwareBlock)$($DeviceEnrollmentBlock)$($DevicePolicyBlock)$($DeviceAppsBlock)$($DeviceGroupsBlock)
" - $DeviceAssetFields = @{ - microsoft_365 = $DeviceIntuneDetailshtml - } - $NewHash = Get-StringHash -String $DeviceIntuneDetailshtml + $DeviceAssetFields = @{ + microsoft_365 = $DeviceIntuneDetailshtml + } + $NewHash = Get-StringHash -String $DeviceIntuneDetailshtml - if (![string]::IsNullOrEmpty($DeviceLayoutId)) { - if ($HuduDevice) { - if (($HuduDevice | Measure-Object).count -eq 1) { - $ExistingAsset = Get-CIPPAzDataTableEntity @HuduAssetCache -Filter "PartitionKey eq 'HuduDevice' and CompanyId eq '$company_id' and RowKey eq '$($HuduDevice.id)'" - $ExistingHash = $ExistingAsset.Hash + if (![string]::IsNullOrEmpty($DeviceLayoutId)) { + if ($HuduDevice) { + if (($HuduDevice | Measure-Object).count -eq 1) { + $ExistingAsset = Get-CIPPAzDataTableEntity @HuduAssetCache -Filter "PartitionKey eq 'HuduDevice' and CompanyId eq '$company_id' and RowKey eq '$($HuduDevice.id)'" + $ExistingHash = $ExistingAsset.Hash - if (!$ExistingAsset -or $ExistingAsset.Hash -ne $NewHash) { - $CompanyResult.Logs.Add("Updating $($HuduDevice.name) in Hudu") - $null = Set-HuduAsset -asset_id $HuduDevice.id -Name $HuduDevice.name -company_id $company_id -asset_layout_id $HuduDevice.asset_layout_id -Fields $DeviceAssetFields -PrimarySerial $Device.serialNumber - $AssetCache = [PSCustomObject]@{ - PartitionKey = 'HuduDevice' - RowKey = [string]$HuduDevice.id - CompanyId = [string]$company_id - Hash = [string]$NewHash + if (!$ExistingAsset -or $ExistingAsset.Hash -ne $NewHash) { + $CompanyResult.Logs.Add("Updating $($HuduDevice.name) in Hudu") + $null = Set-HuduAsset -asset_id $HuduDevice.id -Name $HuduDevice.name -company_id $company_id -asset_layout_id $HuduDevice.asset_layout_id -Fields $DeviceAssetFields -PrimarySerial $Device.serialNumber + $AssetCache = [PSCustomObject]@{ + PartitionKey = 'HuduDevice' + RowKey = [string]$HuduDevice.id + CompanyId = [string]$company_id + Hash = [string]$NewHash + } + Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force } - Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force - $RelHuduUser = $People | Where-Object { $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } - - if ($RelHuduUser) { - $Relation = $HuduRelations | Where-Object { $_.fromable_type -eq 'Asset' -and $_.fromable_id -eq $RelHuduUser.id -and $_.toable_type -eq 'Asset' -and $_toable_id -eq $HuduDevice.id } - if (-not $Relation) { - try { - $null = New-HuduRelation -FromableType 'Asset' -FromableID $RelHuduUser.id -ToableType 'Asset' -ToableID $HuduDevice.id -ea stop - } catch {} + if (![string]::IsNullOrEmpty($Device.userPrincipalName)) { + $RelHuduUser = $People | Where-Object { ($_.fields.label -eq 'Email Address' -and $_.fields.value -eq $Device.userPrincipalName) -or $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } + + if ($RelHuduUser) { + $Relation = $HuduRelations | Where-Object { $_.fromable_type -eq 'Asset' -and $_.fromable_id -eq $RelHuduUser.id -and $_.toable_type -eq 'Asset' -and $_.toable_id -eq $HuduDevice.id } + if (-not $Relation) { + try { + Write-Information "Creating relation between $($RelHuduUser.name) and $($HuduDevice.name)" + $null = New-HuduRelation -FromableType 'Asset' -FromableID $RelHuduUser.id -ToableType 'Asset' -ToableID $HuduDevice.id -ea stop + } catch { + Write-Warning "Failed to create relation between $($RelHuduUser.name) and $($HuduDevice.name): $_" + $CompanyResult.Errors.add("Device $($device.deviceName): Failed to create relation between user and device: $_") + } + } } } + } else { + $CompanyResult.Errors.add("Device $($HuduDevice.name): Multiple devices matched on name or serial ($($device.serialNumber -join ', '))") } } else { - $CompanyResult.Errors.add("Device $($HuduDevice.name): Multiple devices matched on name or serial ($($device.serialNumber -join ', '))") - } - } else { - if ($device.deviceType -in $IntuneDesktopDeviceTypes) { - $DeviceLayoutID = $DesktopsLayout.id - $DeviceCreation = $CreateDevices - } else { - $DeviceLayoutID = $MobilesLayout.id - $DeviceCreation = $CreateMobileDevices - } - if ($DeviceCreation -eq $true) { - $CompanyResult.Logs.Add("Creating $($device.deviceName) in Hudu") - $CreateHuduDevice = (New-HuduAsset -Name $device.deviceName -company_id $company_id -asset_layout_id $DeviceLayoutID -Fields $DeviceAssetFields -PrimarySerial $Device.serialNumber).asset - - if (!$CreateHuduDevice) { - $CompanyResult.Errors.add("Device $($device.deviceName): Failed to create device in Hudu, check your device asset fields for 'Primary Serial'.") + if ($device.deviceType -in $IntuneDesktopDeviceTypes) { + $DeviceLayoutID = $DesktopsLayout.id + $DeviceCreation = $CreateDevices } else { - $AssetCache = [PSCustomObject]@{ - PartitionKey = 'HuduDevice' - RowKey = [string]$CreateHuduDevice.id - CompanyId = [string]$company_id - Hash = [string]$NewHash - } - Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + $DeviceLayoutID = $MobilesLayout.id + $DeviceCreation = $CreateMobileDevices + } + if ($DeviceCreation -eq $true) { + $CompanyResult.Logs.Add("Creating $($device.deviceName) in Hudu") + $CreateHuduDevice = (New-HuduAsset -Name $device.deviceName -company_id $company_id -asset_layout_id $DeviceLayoutID -Fields $DeviceAssetFields -PrimarySerial $Device.serialNumber).asset - $RelHuduUser = $People | Where-Object { $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } - if ($RelHuduUser) { - try { - $null = New-HuduRelation -FromableType 'Asset' -FromableID $RelHuduUser.id -ToableType 'Asset' -ToableID $CreateHuduDevice.id -ea stop - } catch { - # No need to do anything here as its will be when relations already exist. + if (!$CreateHuduDevice) { + $CompanyResult.Errors.add("Device $($device.deviceName): Failed to create device in Hudu, check your device asset fields for 'Primary Serial'.") + } else { + $AssetCache = [PSCustomObject]@{ + PartitionKey = 'HuduDevice' + RowKey = [string]$CreateHuduDevice.id + CompanyId = [string]$company_id + Hash = [string]$NewHash + } + Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + + $RelHuduUser = $People | Where-Object { $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } + if ($RelHuduUser) { + try { + $null = New-HuduRelation -FromableType 'Asset' -FromableID $RelHuduUser.id -ToableType 'Asset' -ToableID $CreateHuduDevice.id -ea stop + } catch { + # No need to do anything here as its will be when relations already exist. + } } } } } } + } catch { + $CompanyResult.Errors.add("Device $($device.deviceName): A Fatal Error occured while processing the device $_") } - } catch { - $CompanyResult.Errors.add("Device $($device.deviceName): A Fatal Error occured while processing the device $_") } - + } else { + $CompanyResult.Logs.Add('Skipping Device Processing - No Device Layout ID') } @@ -931,8 +1068,10 @@ function Invoke-HuduExtensionSync { Write-LogMessage -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -API 'Hudu Sync' -message 'Company: Completed Sync' -level 'Information' $CompanyResult.Logs.Add('Hudu Sync Completed') } catch { - $CompanyResult.Errors.add("Company: A fatal error occured: $_") + Write-Warning "Company: A fatal error occured: $_" + Write-Information $_.InvocationInfo.PositionMessage Write-LogMessage -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -API 'Hudu Sync' -message "Company: A fatal error occured: $_" -level 'Error' + $CompanyResult.Errors.add("Company: A fatal error occured: $_") } return $CompanyResult } diff --git a/openapi.json b/openapi.json index ffb4023947fe..c94c00948d65 100644 --- a/openapi.json +++ b/openapi.json @@ -9655,6 +9655,578 @@ } } }, + "/ExecManageRetentionPolicies": { + "get": { + "description": "List retention policies or get a specific retention policy by name", + "summary": "Get Retention Policies", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string" + }, + "name": "name", + "in": "query", + "description": "Name of specific retention policy to retrieve" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "RetentionPolicyTagLinks": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "description": "Successfully retrieved retention policies" + }, + "400": { + "description": "Bad request - missing required parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + }, + "post": { + "description": "Create, modify, or delete retention policies", + "summary": "Manage Retention Policies", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "CreatePolicies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Name of the retention policy" + }, + "RetentionPolicyTagLinks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of retention tag names to link to this policy" + } + }, + "required": ["Name"] + }, + "description": "Array of retention policies to create" + }, + "ModifyPolicies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Identity": { + "type": "string", + "description": "Identity of the retention policy to modify" + }, + "Name": { + "type": "string", + "description": "New name for the retention policy" + }, + "RetentionPolicyTagLinks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of retention tag names to link to this policy" + }, + "AddTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of retention tag names to add to the policy" + }, + "RemoveTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of retention tag names to remove from the policy" + } + }, + "required": ["Identity"] + }, + "description": "Array of retention policies to modify" + }, + "DeletePolicies": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of retention policy identities to delete" + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "Successfully processed retention policy operations" + }, + "400": { + "description": "Bad request - missing required parameters", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "/ExecManageRetentionTags": { + "get": { + "description": "List retention tags or get a specific retention tag by name", + "summary": "Get Retention Tags", + "tags": [ + "GET" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + }, + { + "required": false, + "schema": { + "type": "string" + }, + "name": "name", + "in": "query", + "description": "Name of specific retention tag to retrieve" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "Type": { + "type": "string" + }, + "RetentionAction": { + "type": "string" + }, + "AgeLimitForRetention": { + "type": "integer" + }, + "RetentionEnabled": { + "type": "boolean" + } + } + } + } + } + }, + "description": "Successfully retrieved retention tags" + }, + "400": { + "description": "Bad request - missing required parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "string", + "description": "Error message" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "string", + "description": "Error message" + } + } + } + } + } + } + } + }, + "post": { + "description": "Create, modify, or delete retention tags", + "summary": "Manage Retention Tags", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "CreateTags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "description": "Name of the retention tag (max 64 characters)" + }, + "Type": { + "type": "string", + "enum": ["All", "Inbox", "SentItems", "DeletedItems", "Drafts", "Outbox", "JunkEmail", "Journal", "SyncIssues", "ConversationHistory", "Personal", "RecoverableItems", "NonIpmRoot", "LegacyArchiveJournals", "Clutter", "Calendar", "Notes", "Tasks", "Contacts", "RssSubscriptions", "ManagedCustomFolder"], + "description": "Type of the retention tag" + }, + "RetentionAction": { + "type": "string", + "enum": ["DeleteAndAllowRecovery", "PermanentlyDelete", "MoveToArchive", "MarkAsPastRetentionLimit"], + "description": "Action to take when retention period expires" + }, + "AgeLimitForRetention": { + "type": "integer", + "minimum": 0, + "maximum": 24855, + "description": "Age limit for retention in days" + }, + "RetentionEnabled": { + "type": "boolean", + "description": "Whether retention is enabled for this tag" + }, + "Comment": { + "type": "string", + "description": "Administrative comment for the tag" + }, + "LocalizedComment": { + "type": "string", + "description": "Localized comment for the tag" + }, + "LocalizedRetentionPolicyTagName": { + "type": "string", + "description": "Localized name for the retention policy tag" + } + }, + "required": ["Name", "Type"] + }, + "description": "Array of retention tags to create" + }, + "ModifyTags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Identity": { + "type": "string", + "description": "Identity of the retention tag to modify" + }, + "Name": { + "type": "string", + "description": "New name for the retention tag" + }, + "RetentionAction": { + "type": "string", + "enum": ["DeleteAndAllowRecovery", "PermanentlyDelete", "MoveToArchive", "MarkAsPastRetentionLimit"], + "description": "Action to take when retention period expires" + }, + "AgeLimitForRetention": { + "type": "integer", + "minimum": 0, + "maximum": 24855, + "description": "Age limit for retention in days" + }, + "RetentionEnabled": { + "type": "boolean", + "description": "Whether retention is enabled for this tag" + }, + "Comment": { + "type": "string", + "description": "Administrative comment for the tag" + }, + "LocalizedComment": { + "type": "string", + "description": "Localized comment for the tag" + }, + "LocalizedRetentionPolicyTagName": { + "type": "string", + "description": "Localized name for the retention policy tag" + } + }, + "required": ["Identity"] + }, + "description": "Array of retention tags to modify" + }, + "DeleteTags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of retention tag identities to delete" + } + } + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "description": "Successfully processed retention tag operations" + }, + "400": { + "description": "Bad request - missing required parameters", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + }, + "/ExecSetMailboxRetentionPolicies": { + "post": { + "description": "Apply a retention policy to one or more mailboxes", + "summary": "Set Mailbox Retention Policies", + "tags": [ + "POST" + ], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "tenantFilter", + "in": "query" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "PolicyName": { + "type": "string", + "description": "Name of the retention policy to apply" + }, + "Mailboxes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of mailbox identities (email addresses or names) to apply the policy to" + } + }, + "required": ["PolicyName", "Mailboxes"] + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of result messages for each mailbox operation" + } + } + } + } + }, + "description": "Successfully processed mailbox retention policy assignments" + }, + "400": { + "description": "Bad request - missing required parameters", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "Results": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, "openapi": "3.1.0", "servers": [ { diff --git a/profile.ps1 b/profile.ps1 index a763fe8b693e..4c02022def7a 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -1,16 +1,4 @@ -# Azure Functions profile.ps1 -# -# This profile.ps1 will get executed every "cold start" of your Function App. -# "cold start" occurs when: -# -# * A Function App starts up for the very first time -# * A Function App starts up after being de-allocated due to inactivity -# -# You can define helper functions, run commands, or specify environment variables -# NOTE: any variables defined that are not environment variables will get reset after the first execution - -# Authenticate with Azure PowerShell using MSI. -# Remove this if you are not planning on using MSI or Azure PowerShell. +Write-Information "CIPP-API Start - PS Version: $($PSVersionTable.PSVersion)" # Import modules @('CIPPCore', 'CippExtensions', 'Az.KeyVault', 'Az.Accounts', 'AzBobbyTables') | ForEach-Object { diff --git a/version_latest.txt b/version_latest.txt index 56b6be4ebb2f..9c78b761ea12 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.3.1 +8.3.2